Feat: Cookie authentication #130

Merged
KevinMidboe merged 21 commits from feat/cookie-authentication into master 2022-08-15 21:59:12 +00:00
26 changed files with 3708 additions and 2652 deletions

View File

@@ -7,7 +7,7 @@
}, },
"main": "webserver/server.js", "main": "webserver/server.js",
"scripts": { "scripts": {
"start": "cross-env SEASONED_CONFIG=conf/development.json PROD=true NODE_PATH=. babel-node src/webserver/server.js", "start": "cross-env SEASONED_CONFIG=conf/development.json NODE_ENV=production NODE_PATH=. babel-node src/webserver/server.js",
"test": "cross-env SEASONED_CONFIG=conf/test.json NODE_PATH=. mocha --require @babel/register --recursive test/unit test/system", "test": "cross-env SEASONED_CONFIG=conf/test.json NODE_PATH=. mocha --require @babel/register --recursive test/unit test/system",
"coverage": "cross-env SEASONED_CONFIG=conf/test.json NODE_PATH=. nyc mocha --require @babel/register --recursive test && nyc report --reporter=text-lcov | coveralls", "coverage": "cross-env SEASONED_CONFIG=conf/test.json NODE_PATH=. nyc mocha --require @babel/register --recursive test && nyc report --reporter=text-lcov | coveralls",
"lint": "./node_modules/.bin/eslint src/", "lint": "./node_modules/.bin/eslint src/",
@@ -18,12 +18,13 @@
}, },
"dependencies": { "dependencies": {
"axios": "^0.18.0", "axios": "^0.18.0",
"bcrypt": "^3.0.6", "bcrypt": "^5.0.1",
"body-parser": "~1.18.2", "body-parser": "~1.18.2",
"cookie-parser": "^1.4.6",
"cross-env": "~5.1.4", "cross-env": "~5.1.4",
"express": "~4.16.0", "express": "~4.16.0",
"form-data": "^2.5.1", "form-data": "^2.5.1",
"jsonwebtoken": "^8.2.0", "jsonwebtoken": "^8.5.1",
"km-moviedb": "^0.2.12", "km-moviedb": "^0.2.12",
"node-cache": "^4.1.1", "node-cache": "^4.1.1",
"node-fetch": "^2.6.0", "node-fetch": "^2.6.0",
@@ -32,7 +33,7 @@
"redis": "^3.0.2", "redis": "^3.0.2",
"request": "^2.87.0", "request": "^2.87.0",
"request-promise": "^4.2", "request-promise": "^4.2",
"sqlite3": "^4.0.0" "sqlite3": "^5.0.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.5.5", "@babel/core": "^7.5.5",

View File

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

View File

@@ -0,0 +1,35 @@
const request = require("request");
const configuration = require('src/config/configuration').getInstance();
const sendSMS = (message) => {
const apiKey = configuration.get('sms', 'apikey')
if (!apiKey) {
console.warning("api key for sms not set, cannot send sms.")
return null
}
const sender = configuration.get('sms', 'sender')
const recipient = configuration.get('sms', 'recipient')
return new Promise((resolve, reject) => {
request.post(
{
url: `https://gatewayapi.com/rest/mtsms?token=${apiKey}`,
json: true,
body: {
sender,
message,
recipients: [{ msisdn: `47${recipient}` }]
}
},
function(err, r, body) {
console.log(err ? err : body);
console.log("sms provider response:", body)
resolve()
}
);
})
}
module.exports = { sendSMS }

View File

@@ -10,6 +10,12 @@ const redisCache = new RedisCache();
const sanitize = string => string.toLowerCase().replace(/[^\w]/gi, ""); const sanitize = string => string.toLowerCase().replace(/[^\w]/gi, "");
function fixedEncodeURIComponent(str) {
return encodeURIComponent(str).replace(/[!'()*]/g, function (c) {
return "%" + c.charCodeAt(0).toString(16).toUpperCase();
});
}
const matchingTitleAndYear = (plex, tmdb) => { const matchingTitleAndYear = (plex, tmdb) => {
let matchingTitle, matchingYear; let matchingTitle, matchingYear;
@@ -121,15 +127,12 @@ class Plex {
findPlexItemByTitleAndYear(title, year) { findPlexItemByTitleAndYear(title, year) {
const query = { title, year }; const query = { title, year };
return this.search(query.title).then(plexSearchResults => { return this.search(title).then(plexResults => {
const matchesInPlex = plexSearchResults.map(plex => const matchesInPlex = plexResults.map(plex =>
this.matchTmdbAndPlexMedia(plex, query) this.matchTmdbAndPlexMedia(plex, query)
); );
const matchesIndex = matchesInPlex.findIndex(el => el === true);
if (matchesInPlex.includes(true) === false) return false; return matchesInPlex != -1 ? plexResults[matchesIndex] : null;
const firstMatchIndex = matchesInPlex.indexOf(true);
return plexSearchResults[firstMatchIndex][0];
}); });
} }
@@ -152,7 +155,7 @@ class Plex {
) )
return false; return false;
const keyUriComponent = encodeURIComponent(matchingObjectInPlex.key); const keyUriComponent = fixedEncodeURIComponent(matchingObjectInPlex.key);
return `https://app.plex.tv/desktop#!/server/${machineIdentifier}/details?key=${keyUriComponent}`; return `https://app.plex.tv/desktop#!/server/${machineIdentifier}/details?key=${keyUriComponent}`;
}); });
} }
@@ -162,7 +165,7 @@ class Plex {
const url = `http://${this.plexIP}:${ const url = `http://${this.plexIP}:${
this.plexPort this.plexPort
}/hubs/search?query=${encodeURIComponent(query)}`; }/hubs/search?query=${fixedEncodeURIComponent(query)}`;
const options = { const options = {
timeout: 20000, timeout: 20000,
headers: { Accept: "application/json" } headers: { Accept: "application/json" }

View File

@@ -1,4 +1,4 @@
const fetch = require('node-fetch'); const fetch = require("node-fetch");
class Tautulli { class Tautulli {
constructor(apiKey, ip, port) { constructor(apiKey, ip, port) {
@@ -8,50 +8,66 @@ class Tautulli {
} }
buildUrlWithCmdAndUserid(cmd, user_id) { buildUrlWithCmdAndUserid(cmd, user_id) {
const url = new URL('api/v2', `http://${this.ip}:${this.port}`) const url = new URL("api/v2", `http://${this.ip}:${this.port}`);
url.searchParams.append('apikey', this.apiKey) url.searchParams.append("apikey", this.apiKey);
url.searchParams.append('cmd', cmd) url.searchParams.append("cmd", cmd);
url.searchParams.append('user_id', user_id) 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) { getPlaysByDayOfWeek(plex_userid, days, y_axis) {
const url = this.buildUrlWithCmdAndUserid('get_plays_by_dayofweek', plex_userid) const url = this.buildUrlWithCmdAndUserid(
url.searchParams.append('time_range', days) "get_plays_by_dayofweek",
url.searchParams.append('y_axis', y_axis) plex_userid
);
url.searchParams.append("time_range", days);
url.searchParams.append("y_axis", y_axis);
return fetch(url.href) return fetch(url.href)
.then(resp => resp.json()) .then(resp => resp.json())
.catch(error => this.logTautulliError(error));
} }
getPlaysByDays(plex_userid, days, y_axis) { getPlaysByDays(plex_userid, days, y_axis) {
const url = this.buildUrlWithCmdAndUserid('get_plays_by_date', plex_userid) const url = this.buildUrlWithCmdAndUserid("get_plays_by_date", plex_userid);
url.searchParams.append('time_range', days) url.searchParams.append("time_range", days);
url.searchParams.append('y_axis', y_axis) url.searchParams.append("y_axis", y_axis);
return fetch(url.href) return fetch(url.href)
.then(resp => resp.json()) .then(resp => resp.json())
.catch(error => this.logTautulliError(error));
} }
watchTimeStats(plex_userid) { watchTimeStats(plex_userid) {
const url = this.buildUrlWithCmdAndUserid('get_user_watch_time_stats', plex_userid) const url = this.buildUrlWithCmdAndUserid(
url.searchParams.append('grouping', 0) "get_user_watch_time_stats",
plex_userid
);
url.searchParams.append("grouping", 0);
return fetch(url.href) return fetch(url.href)
.then(resp => resp.json()) .then(resp => resp.json())
} .catch(error => this.logTautulliError(error));
}
viewHistory(plex_userid) { viewHistory(plex_userid) {
const url = this.buildUrlWithCmdAndUserid('get_history', plex_userid) const url = this.buildUrlWithCmdAndUserid("get_history", plex_userid);
url.searchParams.append('start', 0) url.searchParams.append("start", 0);
url.searchParams.append('length', 50) url.searchParams.append("length", 50);
console.log('fetching url', url.href) console.log("fetching url", url.href);
return fetch(url.href) return fetch(url.href)
.then(resp => resp.json()) .then(resp => resp.json())
.catch(error => this.logTautulliError(error));
} }
} }

View File

@@ -1,29 +1,35 @@
const moviedb = require('km-moviedb'); const moviedb = require("km-moviedb");
const RedisCache = require('src/cache/redis') const RedisCache = require("src/cache/redis");
const redisCache = new RedisCache() const redisCache = new RedisCache();
const { Movie, Show, Person, Credits, ReleaseDates } = require('src/tmdb/types'); const {
Movie,
Show,
Person,
Credits,
ReleaseDates
} = require("src/tmdb/types");
const tmdbErrorResponse = (error, typeString=undefined) => { const tmdbErrorResponse = (error, typeString = undefined) => {
if (error.status === 404) { if (error.status === 404) {
let message = error.response.body.status_message; let message = error.response.body.status_message;
throw { throw {
status: 404, status: 404,
message: message.slice(0, -1) + " in tmdb." message: message.slice(0, -1) + " in tmdb."
} };
} else if (error.status === 401) { } else if (error.status === 401) {
throw { throw {
status: 401, status: 401,
message: error.response.body.status_message message: error.response.body.status_message
} };
} }
throw { throw {
status: 500, status: 500,
message: `An unexpected error occured while fetching ${typeString} from tmdb` message: `An unexpected error occured while fetching ${typeString} from tmdb`
} };
} };
class TMDB { class TMDB {
constructor(apiKey, cache, tmdbLibrary) { constructor(apiKey, cache, tmdbLibrary) {
@@ -31,34 +37,38 @@ class TMDB {
this.cache = cache || redisCache; this.cache = cache || redisCache;
this.cacheTags = { this.cacheTags = {
multiSearch: 'mus', multiSearch: "mus",
movieSearch: 'mos', movieSearch: "mos",
showSearch: 'ss', showSearch: "ss",
personSearch: 'ps', personSearch: "ps",
movieInfo: 'mi', movieInfo: "mi",
movieCredits: 'mc', movieCredits: "mc",
movieReleaseDates: 'mrd', movieReleaseDates: "mrd",
showInfo: 'si', movieImages: "mimg",
showCredits: 'sc', showInfo: "si",
personInfo: 'pi', showCredits: "sc",
miscNowPlayingMovies: 'npm', personInfo: "pi",
miscPopularMovies: 'pm', personCredits: "pc",
miscTopRatedMovies: 'tpm', miscNowPlayingMovies: "npm",
miscUpcomingMovies: 'um', miscPopularMovies: "pm",
tvOnTheAir: 'toa', miscTopRatedMovies: "tpm",
miscPopularTvs: 'pt', miscUpcomingMovies: "um",
miscTopRatedTvs: 'trt', tvOnTheAir: "toa",
miscPopularTvs: "pt",
miscTopRatedTvs: "trt"
}; };
this.defaultTTL = 86400 this.defaultTTL = 86400;
} }
getFromCacheOrFetchFromTmdb(cacheKey, tmdbMethod, query) { getFromCacheOrFetchFromTmdb(cacheKey, tmdbMethod, query) {
return new Promise((resolve, reject) => this.cache.get(cacheKey) return new Promise((resolve, reject) =>
.then(resolve) this.cache
.catch(() => this.tmdb(tmdbMethod, query)) .get(cacheKey)
.then(resolve) .then(resolve)
.catch(error => reject(tmdbErrorResponse(error, tmdbMethod))) .catch(() => this.tmdb(tmdbMethod, query))
) .then(resolve)
.catch(error => reject(tmdbErrorResponse(error, tmdbMethod)))
);
} }
/** /**
@@ -72,9 +82,9 @@ class TMDB {
const query = { id: identifier }; const query = { id: identifier };
const cacheKey = `tmdb/${this.cacheTags.movieInfo}:${identifier}`; const cacheKey = `tmdb/${this.cacheTags.movieInfo}:${identifier}`;
return this.getFromCacheOrFetchFromTmdb(cacheKey, 'movieInfo', query) return this.getFromCacheOrFetchFromTmdb(cacheKey, "movieInfo", query)
.then(movie => this.cache.set(cacheKey, movie, this.defaultTTL)) .then(movie => this.cache.set(cacheKey, movie, this.defaultTTL))
.then(movie => Movie.convertFromTmdbResponse(movie)) .then(movie => Movie.convertFromTmdbResponse(movie));
} }
/** /**
@@ -83,12 +93,12 @@ class TMDB {
* @returns {Promise} movie cast object * @returns {Promise} movie cast object
*/ */
movieCredits(identifier) { movieCredits(identifier) {
const query = { id: identifier } const query = { id: identifier };
const cacheKey = `tmdb/${this.cacheTags.movieCredits}:${identifier}` const cacheKey = `tmdb/${this.cacheTags.movieCredits}:${identifier}`;
return this.getFromCacheOrFetchFromTmdb(cacheKey, 'movieCredits', query) return this.getFromCacheOrFetchFromTmdb(cacheKey, "movieCredits", query)
.then(credits => this.cache.set(cacheKey, credits, this.defaultTTL)) .then(credits => this.cache.set(cacheKey, credits, this.defaultTTL))
.then(credits => Credits.convertFromTmdbResponse(credits)) .then(credits => Credits.convertFromTmdbResponse(credits));
} }
/** /**
@@ -115,18 +125,18 @@ class TMDB {
const query = { id: identifier }; const query = { id: identifier };
const cacheKey = `tmdb/${this.cacheTags.showInfo}:${identifier}`; const cacheKey = `tmdb/${this.cacheTags.showInfo}:${identifier}`;
return this.getFromCacheOrFetchFromTmdb(cacheKey, 'tvInfo', query) return this.getFromCacheOrFetchFromTmdb(cacheKey, "tvInfo", query)
.then(show => this.cache.set(cacheKey, show, this.defaultTTL)) .then(show => this.cache.set(cacheKey, show, this.defaultTTL))
.then(show => Show.convertFromTmdbResponse(show)) .then(show => Show.convertFromTmdbResponse(show));
} }
showCredits(identifier) { showCredits(identifier) {
const query = { id: identifier } const query = { id: identifier };
const cacheKey = `tmdb/${this.cacheTags.showCredits}:${identifier}` const cacheKey = `tmdb/${this.cacheTags.showCredits}:${identifier}`;
return this.getFromCacheOrFetchFromTmdb(cacheKey, 'tvCredits', query) return this.getFromCacheOrFetchFromTmdb(cacheKey, "tvCredits", query)
.then(credits => this.cache.set(cacheKey, credits, this.defaultTTL)) .then(credits => this.cache.set(cacheKey, credits, this.defaultTTL))
.then(credits => Credits.convertFromTmdbResponse(credits)) .then(credits => Credits.convertFromTmdbResponse(credits));
} }
/** /**
@@ -139,16 +149,29 @@ class TMDB {
const query = { id: identifier }; const query = { id: identifier };
const cacheKey = `tmdb/${this.cacheTags.personInfo}:${identifier}`; const cacheKey = `tmdb/${this.cacheTags.personInfo}:${identifier}`;
return this.getFromCacheOrFetchFromTmdb(cacheKey, 'personInfo', query) return this.getFromCacheOrFetchFromTmdb(cacheKey, "personInfo", query)
.then(person => this.cache.set(cacheKey, person, this.defaultTTL)) .then(person => this.cache.set(cacheKey, person, this.defaultTTL))
.then(person => Person.convertFromTmdbResponse(person)) .then(person => Person.convertFromTmdbResponse(person));
} }
multiSearch(search_query, page=1, adult=true) { personCredits(identifier) {
const query = { query: search_query, page: page, include_adult: adult }; const query = { id: identifier };
const cacheKey = `tmdb/${this.cacheTags.multiSearch}:${page}:${search_query}:${adult}`; const cacheKey = `tmdb/${this.cacheTags.personCredits}:${identifier}`;
return this.getFromCacheOrFetchFromTmdb(cacheKey, 'searchMulti', query) return this.getFromCacheOrFetchFromTmdb(
cacheKey,
"personCombinedCredits",
query
)
.then(credits => this.cache.set(cacheKey, credits, this.defaultTTL))
.then(credits => Credits.convertFromTmdbResponse(credits));
}
multiSearch(search_query, page = 1, include_adult = true) {
const query = { query: search_query, page, include_adult };
const cacheKey = `tmdb/${this.cacheTags.multiSearch}:${page}:${search_query}:${include_adult}`;
return this.getFromCacheOrFetchFromTmdb(cacheKey, "searchMulti", query)
.then(response => this.cache.set(cacheKey, response, this.defaultTTL)) .then(response => this.cache.set(cacheKey, response, this.defaultTTL))
.then(response => this.mapResults(response)); .then(response => this.mapResults(response));
} }
@@ -159,13 +182,13 @@ class TMDB {
* @param {Number} page representing pagination of results * @param {Number} page representing pagination of results
* @returns {Promise} dict with query results, current page and total_pages * @returns {Promise} dict with query results, current page and total_pages
*/ */
movieSearch(query, page=1, adult=true) { movieSearch(search_query, page = 1, include_adult = true) {
const tmdbquery = { query: query, page: page, adult: adult }; const tmdbquery = { query: search_query, page, include_adult };
const cacheKey = `tmdb/${this.cacheTags.movieSearch}:${page}:${query}:${adult}`; const cacheKey = `tmdb/${this.cacheTags.movieSearch}:${page}:${search_query}:${include_adult}`;
return this.getFromCacheOrFetchFromTmdb(cacheKey, 'searchMovie', query) return this.getFromCacheOrFetchFromTmdb(cacheKey, "searchMovie", tmdbquery)
.then(response => this.cache.set(cacheKey, response, this.defaultTTL)) .then(response => this.cache.set(cacheKey, response, this.defaultTTL))
.then(response => this.mapResults(response, 'movie')) .then(response => this.mapResults(response, "movie"));
} }
/** /**
@@ -174,13 +197,13 @@ class TMDB {
* @param {Number} page representing pagination of results * @param {Number} page representing pagination of results
* @returns {Promise} dict with query results, current page and total_pages * @returns {Promise} dict with query results, current page and total_pages
*/ */
showSearch(query, page=1) { showSearch(search_query, page = 1, include_adult = true) {
const tmdbquery = { query: query, page: page }; const tmdbquery = { query: search_query, page, include_adult };
const cacheKey = `tmdb/${this.cacheTags.showSearch}:${page}:${query}`; const cacheKey = `tmdb/${this.cacheTags.showSearch}:${page}:${search_query}:${include_adult}`;
return this.getFromCacheOrFetchFromTmdb(cacheKey, 'searchTv', query) return this.getFromCacheOrFetchFromTmdb(cacheKey, "searchTv", tmdbquery)
.then(response => this.cache.set(cacheKey, response, this.defaultTTL)) .then(response => this.cache.set(cacheKey, response, this.defaultTTL))
.then(response => this.mapResults(response, 'show')) .then(response => this.mapResults(response, "show"));
} }
/** /**
@@ -189,14 +212,13 @@ class TMDB {
* @param {Number} page representing pagination of results * @param {Number} page representing pagination of results
* @returns {Promise} dict with query results, current page and total_pages * @returns {Promise} dict with query results, current page and total_pages
*/ */
personSearch(query, page=1) { personSearch(search_query, page = 1, include_adult = true) {
const tmdbquery = { query: search_query, page, include_adult };
const cacheKey = `tmdb/${this.cacheTags.personSearch}:${page}:${search_query}:${include_adult}`;
const tmdbquery = { query: query, page: page, include_adult: true }; return this.getFromCacheOrFetchFromTmdb(cacheKey, "searchPerson", tmdbquery)
const cacheKey = `tmdb/${this.cacheTags.personSearch}:${page}:${query}:${include_adult}`;
return this.getFromCacheOrFetchFromTmdb(cacheKey, 'searchPerson', query)
.then(response => this.cache.set(cacheKey, response, this.defaultTTL)) .then(response => this.cache.set(cacheKey, response, this.defaultTTL))
.then(response => this.mapResults(response, 'person')) .then(response => this.mapResults(response, "person"));
} }
movieList(listname, page = 1) { movieList(listname, page = 1) {
@@ -205,16 +227,16 @@ class TMDB {
return this.getFromCacheOrFetchFromTmdb(cacheKey, listname, query) return this.getFromCacheOrFetchFromTmdb(cacheKey, listname, query)
.then(response => this.cache.set(cacheKey, response, this.defaultTTL)) .then(response => this.cache.set(cacheKey, response, this.defaultTTL))
.then(response => this.mapResults(response, 'movie')) .then(response => this.mapResults(response, "movie"));
} }
showList(listname, page = 1) { showList(listname, page = 1) {
const query = { page: page }; const query = { page: page };
const cacheKey = `tmdb/${this.cacheTags[listname]}:${page}`; const cacheKey = `tmdb/${this.cacheTags[listname]}:${page}`;
return this.getFromCacheOrFetchFromTmdb(cacheKey, listName, query) return this.getFromCacheOrFetchFromTmdb(cacheKey, listName, query)
.then(response => this.cache.set(cacheKey, response, this.defaultTTL)) .then(response => this.cache.set(cacheKey, response, this.defaultTTL))
.then(response => this.mapResults(response, 'show')) .then(response => this.mapResults(response, "show"));
} }
/** /**
@@ -223,27 +245,26 @@ class TMDB {
* @param {String} The type declared in listSearch. * @param {String} The type declared in listSearch.
* @returns {Promise} dict with tmdb results, mapped as movie/show objects. * @returns {Promise} dict with tmdb results, mapped as movie/show objects.
*/ */
mapResults(response, type=undefined) { mapResults(response, type = undefined) {
let results = response.results.map(result => { let results = response.results.map(result => {
if (type === 'movie' || result.media_type === 'movie') { if (type === "movie" || result.media_type === "movie") {
const movie = Movie.convertFromTmdbResponse(result) const movie = Movie.convertFromTmdbResponse(result);
return movie.createJsonResponse() return movie.createJsonResponse();
} else if (type === 'show' || result.media_type === 'tv') { } else if (type === "show" || result.media_type === "tv") {
const show = Show.convertFromTmdbResponse(result) const show = Show.convertFromTmdbResponse(result);
return show.createJsonResponse() return show.createJsonResponse();
} else if (type === 'person' || result.media_type === 'person') { } else if (type === "person" || result.media_type === "person") {
const person = Person.convertFromTmdbResponse(result) const person = Person.convertFromTmdbResponse(result);
return person.createJsonResponse() return person.createJsonResponse();
} }
}) });
return { return {
results: results, results: results,
page: response.page, page: response.page,
total_results: response.total_results, total_results: response.total_results,
total_pages: response.total_pages total_pages: response.total_pages
} };
} }
/** /**
@@ -252,25 +273,22 @@ class TMDB {
* @param {Object} argument argument to function being called * @param {Object} argument argument to function being called
* @returns {Promise} succeeds if callback succeeds * @returns {Promise} succeeds if callback succeeds
*/ */
tmdb(method, argument) { tmdb(method, argument) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const callback = (error, reponse) => { const callback = (error, reponse) => {
if (error) { if (error) {
return reject(error); return reject(error);
} }
resolve(reponse); resolve(reponse);
}; };
if (!argument) {
this.tmdbLibrary[method](callback);
} else {
this.tmdbLibrary[method](argument, callback);
}
});
}
if (!argument) {
this.tmdbLibrary[method](callback);
} else {
this.tmdbLibrary[method](argument, callback);
}
});
}
} }
module.exports = TMDB; module.exports = TMDB;

View File

@@ -1,20 +1,55 @@
import Movie from "./movie";
import Show from "./show";
class Credits { class Credits {
constructor(id, cast=[], crew=[]) { constructor(id, cast = [], crew = []) {
this.id = id; this.id = id;
this.cast = cast; this.cast = cast;
this.crew = crew; this.crew = crew;
this.type = 'credits'; this.type = "credits";
} }
static convertFromTmdbResponse(response) { static convertFromTmdbResponse(response) {
const { id, cast, crew } = response; const { id, cast, crew } = response;
const allCast = cast.map(cast => const allCast = cast.map(cast => {
new CastMember(cast.character, cast.gender, cast.id, cast.name, cast.profile_path)) if (cast["media_type"]) {
const allCrew = crew.map(crew => if (cast.media_type === "movie") {
new CrewMember(crew.department, crew.gender, crew.id, crew.job, crew.name, crew.profile_path)) return CreditedMovie.convertFromTmdbResponse(cast);
} else if (cast.media_type === "tv") {
return CreditedShow.convertFromTmdbResponse(cast);
}
}
return new Credits(id, allCast, allCrew) return new CastMember(
cast.character,
cast.gender,
cast.id,
cast.name,
cast.profile_path
);
});
const allCrew = crew.map(crew => {
if (cast["media_type"]) {
if (cast.media_type === "movie") {
return CreditedMovie.convertFromTmdbResponse(cast);
} else if (cast.media_type === "tv") {
return CreditedShow.convertFromTmdbResponse(cast);
}
}
return new CrewMember(
crew.department,
crew.gender,
crew.id,
crew.job,
crew.name,
crew.profile_path
);
});
return new Credits(id, allCast, allCrew);
} }
createJsonResponse() { createJsonResponse() {
@@ -22,7 +57,7 @@ class Credits {
id: this.id, id: this.id,
cast: this.cast.map(cast => cast.createJsonResponse()), cast: this.cast.map(cast => cast.createJsonResponse()),
crew: this.crew.map(crew => crew.createJsonResponse()) crew: this.crew.map(crew => crew.createJsonResponse())
} };
} }
} }
@@ -33,7 +68,7 @@ class CastMember {
this.id = id; this.id = id;
this.name = name; this.name = name;
this.profile_path = profile_path; this.profile_path = profile_path;
this.type = 'cast member'; this.type = "person";
} }
createJsonResponse() { createJsonResponse() {
@@ -44,7 +79,7 @@ class CastMember {
name: this.name, name: this.name,
profile_path: this.profile_path, profile_path: this.profile_path,
type: this.type type: this.type
} };
} }
} }
@@ -56,7 +91,7 @@ class CrewMember {
this.job = job; this.job = job;
this.name = name; this.name = name;
this.profile_path = profile_path; this.profile_path = profile_path;
this.type = 'crew member'; this.type = "person";
} }
createJsonResponse() { createJsonResponse() {
@@ -68,8 +103,11 @@ class CrewMember {
name: this.name, name: this.name,
profile_path: this.profile_path, profile_path: this.profile_path,
type: this.type type: this.type
} };
} }
} }
class CreditedMovie extends Movie {}
class CreditedShow extends Show {}
module.exports = Credits; module.exports = Credits;

View File

@@ -1,23 +1,54 @@
class Person { class Person {
constructor(id, name, poster=undefined, birthday=undefined, deathday=undefined, constructor(
adult=undefined, knownForDepartment=undefined) { id,
name,
poster = undefined,
birthday = undefined,
deathday = undefined,
adult = undefined,
placeOfBirth = undefined,
biography = undefined,
knownForDepartment = undefined
) {
this.id = id; this.id = id;
this.name = name; this.name = name;
this.poster = poster; this.poster = poster;
this.birthday = birthday; this.birthday = birthday;
this.deathday = deathday; this.deathday = deathday;
this.adult = adult; this.adult = adult;
this.placeOfBirth = placeOfBirth;
this.biography = biography;
this.knownForDepartment = knownForDepartment; this.knownForDepartment = knownForDepartment;
this.type = 'person'; this.type = "person";
} }
static convertFromTmdbResponse(response) { static convertFromTmdbResponse(response) {
const { id, name, profile_path, birthday, deathday, adult, known_for_department } = response; const {
id,
name,
profile_path,
birthday,
deathday,
adult,
place_of_birth,
biography,
known_for_department
} = response;
const birthDay = new Date(birthday) const birthDay = new Date(birthday);
const deathDay = deathday ? new Date(deathday) : null const deathDay = deathday ? new Date(deathday) : null;
return new Person(id, name, profile_path, birthDay, deathDay, adult, known_for_department) return new Person(
id,
name,
profile_path,
birthDay,
deathDay,
adult,
place_of_birth,
biography,
known_for_department
);
} }
createJsonResponse() { createJsonResponse() {
@@ -27,10 +58,12 @@ class Person {
poster: this.poster, poster: this.poster,
birthday: this.birthday, birthday: this.birthday,
deathday: this.deathday, deathday: this.deathday,
place_of_birth: this.placeOfBirth,
biography: this.biography,
known_for_department: this.knownForDepartment, known_for_department: this.knownForDepartment,
adult: this.adult, adult: this.adult,
type: this.type type: this.type
} };
} }
} }

View File

@@ -1,26 +1,25 @@
const User = require('src/user/user'); const User = require("src/user/user");
const jwt = require('jsonwebtoken'); const jwt = require("jsonwebtoken");
class Token { class Token {
constructor(user, admin=false) { constructor(user, admin = false, settings = null) {
this.user = user; this.user = user;
this.admin = admin; this.admin = admin;
this.settings = settings;
} }
/** /**
* Generate a new token. * Generate a new token.
* @param {String} secret a cipher of the token * @param {String} secret a cipher of the token
* @returns {String} * @returns {String}
*/ */
toString(secret) { toString(secret) {
const username = this.user.username; const { user, admin, settings } = this;
const admin = this.admin;
let data = { username }
if (admin) let data = { username: user.username, settings };
data = { ...data, admin } if (admin) data["admin"] = admin;
return jwt.sign(data, secret, { expiresIn: '90d' }); return jwt.sign(data, secret, { expiresIn: "90d" });
} }
/** /**
@@ -30,15 +29,12 @@ class Token {
* @returns {Token} * @returns {Token}
*/ */
static fromString(jwtToken, secret) { static fromString(jwtToken, secret) {
let username = null; const token = jwt.verify(jwtToken, secret, { clockTolerance: 10000 });
if (token.username == null) throw new Error("Malformed token");
const token = jwt.verify(jwtToken, secret, { clockTolerance: 10000 }) const { username, admin, settings } = token;
if (token.username === undefined || token.username === null) const user = new User(username);
throw new Error('Malformed token') return new Token(user, admin, settings);
username = token.username
const user = new User(username)
return new Token(user)
} }
} }

View File

@@ -1,219 +1,256 @@
const assert = require('assert'); 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 = ?', link: "update settings set plex_userid = ? where user_name = ?",
unlink: 'update settings set plex_userid = null where user_name = ?', unlink: "update settings set plex_userid = null where user_name = ?",
createSettings: 'insert into settings (user_name) values (?)', createSettings: "insert into settings (user_name) values (?)",
updateSettings: 'update settings set user_name = ?, dark_mode = ?, emoji = ?', updateSettings:
getSettings: 'select * from settings where user_name = ?' "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 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)) .then(() => this.database.run(this.queries.create, user.username))
.catch((error) => { .catch(error => {
if (error.name === 'AssertionError' || error.message.endsWith('user_name')) { if (
throw new Error('That username is already registered'); 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. * Retrieve a password from a database.
* @param {User} user the user you want to retrieve the password * @param {User} user the user you want to retrieve the password
* @returns {Promise} * @returns {Promise}
*/ */
retrieveHash(user) { retrieveHash(user) {
return this.database.get(this.queries.retrieveHash, user.username) return this.database
.get(this.queries.retrieveHash, user.username)
.then(row => { .then(row => {
assert(row, 'The user does not exist.'); assert(row, "The user does not exist.");
return row.password; 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. * Change a user's password in a database.
* @param {User} user the user you want to create * @param {User} user the user you want to create
* @param {String} password the new password you want to change * @param {String} password the new password you want to change
* @returns {Promise} * @returns {Promise}
*/ */
changePassword(user, password) { 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 * Link plex userid with seasoned user
* @param {String} username the user you want to lunk plex userid with * @param {String} username the user you want to lunk plex userid with
* @param {Number} plexUserID plex unique id * @param {Number} plexUserID plex unique id
* @returns {Promsie} * @returns {Promsie}
*/ */
linkPlexUserId(username, plexUserID) { linkPlexUserId(username, plexUserID) {
return new Promise((resolve, reject) => { 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)) .then(row => resolve(row))
.catch(error => { .catch(error => {
// TODO log this unknown db error // TODO log this unknown db error
console.log('db error', error) console.error("db error", error);
reject({ reject({
status: 500, status: 500,
message: 'An unexpected error occured while linking plex and seasoned accounts', message:
source: 'seasoned database' "An unexpected error occured while linking plex and seasoned accounts",
}) source: "seasoned database"
}) });
}) });
});
} }
/** /**
* Unlink plex userid with seasoned user * Unlink plex userid with seasoned user
* @param {User} user the user you want to lunk plex userid with * @param {User} user the user you want to lunk plex userid with
* @returns {Promsie} * @returns {Promsie}
*/ */
unlinkPlexUserId(username) { unlinkPlexUserId(username) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.database.run(this.queries.unlink, username) this.database
.run(this.queries.unlink, username)
.then(row => resolve(row)) .then(row => resolve(row))
.catch(error => { .catch(error => {
// TODO log this unknown db error // TODO log this unknown db error
console.log('db error', error) console.log("db error", error);
reject({ reject({
status: 500, status: 500,
message: 'An unexpected error occured while unlinking plex and seasoned accounts', message:
source: 'seasoned database' "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 * Check if the user has boolean flag set for admin in database
* @param {User} user object * @param {User} user object
* @returns {Promsie} * @returns {Promsie}
*/ */
checkAdmin(user) { checkAdmin(user) {
return this.database.get(this.queries.getAdminStateByUser, user.username) return this.database
.then((row) => row.admin); .get(this.queries.getAdminStateByUser, user.username)
.then(row => row.admin);
} }
/** /**
* Get settings for user matching string username * Get settings for user matching string username
* @param {String} username * @param {String} username
* @returns {Promsie} * @returns {Promsie}
*/ */
getSettings(username) { getSettings(username) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.database.get(this.queries.getSettings, username) this.database
.then(async (row) => { .get(this.queries.getSettings, username)
.then(async row => {
if (row == null) { 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) { if (userExistsWithUsername !== undefined) {
try { try {
resolve(this.dbCreateSettings(username)) resolve(this.dbCreateSettings(username));
} catch (error) { } catch (error) {
reject(error) reject(error);
} }
} else { } else {
reject({ reject({
status: 404, status: 404,
message: 'User not found, no settings to get' message: "User not found, no settings to get"
}) });
} }
} }
resolve(row) resolve(row);
}) })
.catch(error => { .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({ reject({
status: 500, status: 500,
message: 'An unexpected error occured while fetching settings for your account', message:
source: 'seasoned database' "An unexpected error occured while fetching settings for your account",
}) source: "seasoned database"
}) });
}) });
});
} }
/** /**
* Update settings values for user matching string username * Update settings values for user matching string username
* @param {String} username * @param {String} username
* @param {String} dark_mode * @param {String} dark_mode
* @param {String} emoji * @param {String} emoji
* @returns {Promsie} * @returns {Promsie}
*/ */
updateSettings(username, dark_mode=undefined, emoji=undefined) { updateSettings(username, dark_mode = undefined, emoji = undefined) {
const settings = this.getSettings(username) const settings = this.getSettings(username);
dark_mode = dark_mode !== undefined ? dark_mode : settings.dark_mode dark_mode = dark_mode !== undefined ? dark_mode : settings.dark_mode;
emoji = emoji !== undefined ? emoji : settings.emoji emoji = emoji !== undefined ? emoji : settings.emoji;
return this.dbUpdateSettings(username, dark_mode, emoji) return this.dbUpdateSettings(username, dark_mode, emoji).catch(error => {
.catch(error => { if (error.status && error.message) {
if (error.status && error.message) { return error;
return error }
}
return { return {
status: 500, status: 500,
message: 'An unexpected error occured while updating settings for your account' message:
} "An unexpected error occured while updating settings for your account"
}) };
});
} }
/** /**
* Helper function for creating settings in the database * Helper function for creating settings in the database
* @param {String} username * @param {String} username
* @returns {Promsie} * @returns {Promsie}
*/ */
dbCreateSettings(username) { 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)) .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 * Helper function for updating settings in the database
* @param {String} username * @param {String} username
* @returns {Promsie} * @returns {Promsie}
*/ */
dbUpdateSettings(username, dark_mode, emoji) { dbUpdateSettings(username, dark_mode, emoji) {
return new Promise((resolve, reject) => return new Promise((resolve, reject) =>
this.database.run(this.queries.updateSettings, [username, dark_mode, emoji]) this.database
.then(row => resolve(row))) .run(this.queries.updateSettings, [username, dark_mode, emoji])
.then(row => resolve(row))
);
} }
} }
const rejectUnexpectedDatabaseError = (
const rejectUnexpectedDatabaseError = (message, status, error, reject=null) => { message,
console.error(error) status,
error,
reject = null
) => {
console.error(error);
const body = { const body = {
status, status,
message, message,
source: 'seasoned database' source: "seasoned database"
} };
if (reject == null) { if (reject == null) {
return new Promise((resolve, reject) => reject(body)) return new Promise((resolve, reject) => reject(body));
} }
reject(body) reject(body);
} };
module.exports = UserRepository; module.exports = UserRepository;

View File

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

View File

@@ -1,11 +1,14 @@
const express = require("express"); const express = require("express");
const Raven = require("raven"); const Raven = require("raven");
const cookieParser = require("cookie-parser");
const bodyParser = require("body-parser"); const bodyParser = require("body-parser");
const tokenToUser = require("./middleware/tokenToUser");
const configuration = require("src/config/configuration").getInstance();
const reqTokenToUser = require("./middleware/reqTokenToUser");
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 mustHaveAccountLinkedToPlex = require("./middleware/mustHaveAccountLinkedToPlex");
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 tautulli = require("./controllers/user/viewHistory.js");
@@ -18,6 +21,7 @@ Raven.config(configuration.get("raven", "DSN")).install();
const app = express(); // define our app using express const app = express(); // define our app using express
app.use(Raven.requestHandler()); app.use(Raven.requestHandler());
app.use(bodyParser.json()); app.use(bodyParser.json());
app.use(cookieParser());
const router = express.Router(); const router = express.Router();
const allowedOrigins = configuration.get("webserver", "origins"); const allowedOrigins = configuration.get("webserver", "origins");
@@ -26,31 +30,34 @@ const allowedOrigins = configuration.get("webserver", "origins");
// router.use(bodyParser.json()); // router.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.urlencoded({ extended: true }));
/* Decode the Authorization header if provided */ /* Check header and cookie for authentication and set req.loggedInUser */
router.use(tokenToUser); router.use(reqTokenToUser);
// TODO: Should have a separate middleware/router for handling headers. // TODO: Should have a separate middleware/router for handling headers.
router.use((req, res, next) => { router.use((req, res, next) => {
// TODO add logging of all incoming // TODO add logging of all incoming
const origin = req.headers.origin; // const origin = req.headers.origin;
if (allowedOrigins.indexOf(origin) > -1) { // if (allowedOrigins.indexOf(origin) > -1) {
res.setHeader("Access-Control-Allow-Origin", origin); // res.setHeader("Access-Control-Allow-Origin", origin);
} // }
res.header( res.header(
"Access-Control-Allow-Headers", "Access-Control-Allow-Headers",
"Content-Type, Authorization, loggedinuser" "Content-Type, Authorization, loggedinuser, set-cookie"
); );
res.header("Access-Control-Allow-Methods", "POST, GET, PUT");
res.header("Access-Control-Allow-Credentials", "true");
res.header("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS");
next(); next();
}); });
router.get("/", function mainHandler(req, res) { router.get("/", (req, res) => {
throw new Error("Broke!"); res.send("welcome to seasoned api");
}); });
app.use(Raven.errorHandler()); app.use(Raven.errorHandler());
app.use(function onError(err, req, res, next) { app.use((err, req, res, next) => {
res.statusCode = 500; res.statusCode = 500;
res.end(res.sentry + "\n"); res.end(res.sentry + "\n");
}); });
@@ -60,6 +67,7 @@ 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.post("/v1/user/logout", require("./controllers/user/logout.js"));
router.get( router.get(
"/v1/user/settings", "/v1/user/settings",
mustBeAuthenticated, mustBeAuthenticated,
@@ -137,20 +145,23 @@ router.get("/v2/movie/now_playing", listController.nowPlayingMovies);
router.get("/v2/movie/popular", listController.popularMovies); router.get("/v2/movie/popular", listController.popularMovies);
router.get("/v2/movie/top_rated", listController.topRatedMovies); router.get("/v2/movie/top_rated", listController.topRatedMovies);
router.get("/v2/movie/upcoming", listController.upcomingMovies); router.get("/v2/movie/upcoming", listController.upcomingMovies);
router.get("/v2/show/now_playing", listController.nowPlayingShows);
router.get("/v2/show/popular", listController.popularShows);
router.get("/v2/show/top_rated", listController.topRatedShows);
router.get("/v2/movie/:id/credits", require("./controllers/movie/credits.js")); router.get("/v2/movie/:id/credits", require("./controllers/movie/credits.js"));
router.get( router.get(
"/v2/movie/:id/release_dates", "/v2/movie/:id/release_dates",
require("./controllers/movie/releaseDates.js") require("./controllers/movie/releaseDates.js")
); );
router.get("/v2/show/:id/credits", require("./controllers/show/credits.js"));
router.get("/v2/movie/:id", require("./controllers/movie/info.js")); router.get("/v2/movie/:id", require("./controllers/movie/info.js"));
router.get("/v2/show/now_playing", listController.nowPlayingShows);
router.get("/v2/show/popular", listController.popularShows);
router.get("/v2/show/top_rated", listController.topRatedShows);
router.get("/v2/show/:id/credits", require("./controllers/show/credits.js"));
router.get("/v2/show/:id", require("./controllers/show/info.js")); router.get("/v2/show/:id", require("./controllers/show/info.js"));
router.get(
"/v2/person/:id/credits",
require("./controllers/person/credits.js")
);
router.get("/v2/person/:id", require("./controllers/person/info.js")); router.get("/v2/person/:id", require("./controllers/person/info.js"));
/** /**

View File

@@ -0,0 +1,26 @@
const configuration = require("src/config/configuration").getInstance();
const TMDB = require("src/tmdb/tmdb");
const tmdb = new TMDB(configuration.get("tmdb", "apiKey"));
const personCreditsController = (req, res) => {
const personId = req.params.id;
return tmdb
.personCredits(personId)
.then(credits => res.send(credits))
.catch(error => {
const { status, message } = error;
if (status && message) {
res.status(status).send({ success: false, message });
} else {
// TODO log unhandled errors
console.log("caugth show credits controller error", error);
res.status(500).send({
message: "An unexpected error occured while requesting person credits"
});
}
});
};
module.exports = personCreditsController;

View File

@@ -1,6 +1,19 @@
const configuration = require('src/config/configuration').getInstance(); const configuration = require("src/config/configuration").getInstance();
const TMDB = require('src/tmdb/tmdb'); const TMDB = require("src/tmdb/tmdb");
const tmdb = new TMDB(configuration.get('tmdb', 'apiKey')); const tmdb = new TMDB(configuration.get("tmdb", "apiKey"));
function handleError(error, res) {
const { status, message } = error;
if (status && message) {
res.status(status).send({ success: false, message });
} else {
console.log("caught personinfo controller error", error);
res.status(500).send({
message: "An unexpected error occured while requesting person info."
});
}
}
/** /**
* Controller: Retrieve information for a person * Controller: Retrieve information for a person
@@ -9,15 +22,28 @@ const tmdb = new TMDB(configuration.get('tmdb', 'apiKey'));
* @returns {Callback} * @returns {Callback}
*/ */
function personInfoController(req, res) { async function personInfoController(req, res) {
const personId = req.params.id; const personId = req.params.id;
let { credits } = req.query;
arguments;
credits && credits.toLowerCase() === "true"
? (credits = true)
: (credits = false);
tmdb.personInfo(personId) let tmdbQueue = [tmdb.personInfo(personId)];
.then(person => res.send(person.createJsonResponse())) if (credits) tmdbQueue.push(tmdb.personCredits(personId));
.catch(error => {
res.status(404).send({ success: false, message: error.message }); try {
}); const [Person, Credits] = await Promise.all(tmdbQueue);
const person = Person.createJsonResponse();
if (credits) person.credits = Credits.createJsonResponse();
return res.send(person);
} catch (error) {
handleError(error, res);
}
} }
module.exports = personInfoController; module.exports = personInfoController;

View File

@@ -3,6 +3,7 @@ const TMDB = require("src/tmdb/tmdb");
const RequestRepository = require("src/request/request"); const RequestRepository = require("src/request/request");
const tmdb = new TMDB(configuration.get("tmdb", "apiKey")); const tmdb = new TMDB(configuration.get("tmdb", "apiKey"));
const request = new RequestRepository(); const request = new RequestRepository();
// const { sendSMS } = require("src/notifications/sms");
const tmdbMovieInfo = id => { const tmdbMovieInfo = id => {
return tmdb.movieInfo(id); return tmdb.movieInfo(id);
@@ -47,9 +48,14 @@ function requestTmdbIdController(req, res) {
mediaFunction(id) mediaFunction(id)
// .catch((error) => { console.error(error); res.status(404).send({ success: false, error: 'Id not found' }) }) // .catch((error) => { console.error(error); res.status(404).send({ success: false, error: 'Id not found' }) })
.then(tmdbMedia => .then(tmdbMedia => {
request.requestFromTmdb(tmdbMedia, ip, user_agent, username) request.requestFromTmdb(tmdbMedia, ip, user_agent, username);
)
// TODO enable SMS
// const url = `https://request.movie?${tmdbMedia.type}=${tmdbMedia.id}`;
// const message = `${tmdbMedia.title} (${tmdbMedia.year}) requested!\n${url}`;
// sendSMS(message);
})
.then(() => .then(() =>
res.send({ success: true, message: "Request has been submitted." }) res.send({ success: true, message: "Request has been submitted." })
) )

View File

@@ -11,15 +11,16 @@ const searchHistory = new SearchHistory();
* @returns {Callback} * @returns {Callback}
*/ */
function movieSearchController(req, res) { function movieSearchController(req, res) {
const { query, page } = req.query; const { query, page, adult } = req.query;
const username = req.loggedInUser ? req.loggedInUser.username : null; const username = req.loggedInUser ? req.loggedInUser.username : null;
const includeAdult = adult == "true" ? true : false;
if (username) { if (username) {
return searchHistory.create(username, query); searchHistory.create(username, query);
} }
tmdb return tmdb
.movieSearch(query, page) .movieSearch(query, page, includeAdult)
.then(movieSearchResults => res.send(movieSearchResults)) .then(movieSearchResults => res.send(movieSearchResults))
.catch(error => { .catch(error => {
const { status, message } = error; const { status, message } = error;

View File

@@ -11,18 +11,17 @@ const searchHistory = new SearchHistory();
* @returns {Callback} * @returns {Callback}
*/ */
function personSearchController(req, res) { function personSearchController(req, res) {
const { query, page } = req.query; const { query, page, adult } = req.query;
const username = req.loggedInUser ? req.loggedInUser.username : null; const username = req.loggedInUser ? req.loggedInUser.username : null;
const includeAdult = adult == "true" ? true : false;
if (username) { if (username) {
return searchHistory.create(username, query); searchHistory.create(username, query);
} }
tmdb return tmdb
.personSearch(query, page) .personSearch(query, page, includeAdult)
.then(person => { .then(persons => res.send(persons))
res.send(person);
})
.catch(error => { .catch(error => {
const { status, message } = error; const { status, message } = error;

View File

@@ -11,17 +11,16 @@ const searchHistory = new SearchHistory();
* @returns {Callback} * @returns {Callback}
*/ */
function showSearchController(req, res) { function showSearchController(req, res) {
const { query, page } = req.query; const { query, page, adult } = req.query;
const username = req.loggedInUser ? req.loggedInUser.username : null; const username = req.loggedInUser ? req.loggedInUser.username : null;
const includeAdult = adult == "true" ? true : false;
Promise.resolve() if (username) {
.then(() => { searchHistory.create(username, query);
if (username) { }
return searchHistory.create(username, query);
} return tmdb
return null; .showSearch(query, page, includeAdult)
})
.then(() => tmdb.showSearch(query, page))
.then(shows => { .then(shows => {
res.send(shows); res.send(shows);
}) })

View File

@@ -1,36 +1,61 @@
const User = require('src/user/user'); const User = require("src/user/user");
const Token = require('src/user/token'); const Token = require("src/user/token");
const UserSecurity = require('src/user/userSecurity'); const UserSecurity = require("src/user/userSecurity");
const UserRepository = require('src/user/userRepository'); const UserRepository = require("src/user/userRepository");
const configuration = require('src/config/configuration').getInstance(); const configuration = require("src/config/configuration").getInstance();
const secret = configuration.get('authentication', 'secret'); 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" // 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. // catch including the, maybe sensitive, error message.
const isProduction = process.env.NODE_ENV === "production";
const cookieOptions = {
httpOnly: false,
secure: isProduction,
maxAge: 90 * 24 * 3600000, // 90 days
sameSite: isProduction ? "Strict" : "Lax"
};
/** /**
* 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
* @param {Response} res * @param {Response} res
* @returns {Callback} * @returns {Callback}
*/ */
function loginController(req, res) { async function loginController(req, res) {
const user = new User(req.body.username); const user = new User(req.body.username);
const password = req.body.password; const password = req.body.password;
userSecurity.login(user, password) try {
.then(() => userRepository.checkAdmin(user)) const [loggedIn, isAdmin, settings] = await Promise.all([
.then(checkAdmin => { userSecurity.login(user, password),
const isAdmin = checkAdmin === 1 ? true : false; userRepository.checkAdmin(user),
const token = new Token(user, isAdmin).toString(secret); userRepository.getSettings(user.username)
res.send({ success: true, token }); ]);
})
.catch(error => { if (!loggedIn) {
res.status(401).send({ success: false, message: error.message }); return res.status(503).send({
success: false,
message: "Unexpected error! Unable to create user."
}); });
}
const token = new Token(
user,
isAdmin === 1 ? true : false,
settings
).toString(secret);
return res.cookie("authorization", token, cookieOptions).status(200).send({
success: true,
message: "Welcome to request.movie!"
});
} catch (error) {
return res.status(401).send({ success: false, message: error.message });
}
} }
module.exports = loginController; module.exports = loginController;

View File

@@ -0,0 +1,16 @@
/**
* Controller: Log out a user (destroy authorization token)
* @param {Request} req http request variable
* @param {Response} res
* @returns {Callback}
*/
async function logoutController(req, res) {
res.clearCookie("authorization");
return res.status(200).send({
success: true,
message: "Logged out, see you later!"
});
}
module.exports = logoutController;

View File

@@ -1,13 +1,21 @@
const User = require('src/user/user'); const User = require("src/user/user");
const Token = require('src/user/token'); const Token = require("src/user/token");
const UserSecurity = require('src/user/userSecurity'); const UserSecurity = require("src/user/userSecurity");
const UserRepository = require('src/user/userRepository'); const UserRepository = require("src/user/userRepository");
const configuration = require('src/config/configuration').getInstance(); const configuration = require("src/config/configuration").getInstance();
const secret = configuration.get('authentication', 'secret'); const secret = configuration.get("authentication", "secret");
const userSecurity = new UserSecurity(); const userSecurity = new UserSecurity();
const userRepository = new UserRepository(); const userRepository = new UserRepository();
const isProduction = process.env.NODE_ENV === "production";
const cookieOptions = {
httpOnly: false,
secure: isProduction,
maxAge: 90 * 24 * 3600000, // 90 days
sameSite: isProduction ? "Strict" : "Lax"
};
/** /**
* Controller: Register a new user * Controller: Register a new user
* @param {Request} req http request variable * @param {Request} req http request variable
@@ -15,21 +23,25 @@ const userRepository = new UserRepository();
* @returns {Callback} * @returns {Callback}
*/ */
function registerController(req, res) { function registerController(req, res) {
const user = new User(req.body.username, req.body.email); const user = new User(req.body.username, req.body.email);
const password = req.body.password; const password = req.body.password;
userSecurity.createNewUser(user, password) userSecurity
.then(() => userRepository.checkAdmin(user)) .createNewUser(user, password)
.then(checkAdmin => { .then(() => {
const isAdmin = checkAdmin === 1 ? true : false; const token = new Token(user, false).toString(secret);
const token = new Token(user, isAdmin).toString(secret);
res.send({ return res
success: true, message: 'Welcome to Seasoned!', token .cookie("authorization", token, cookieOptions)
}); .status(200)
}) .send({
.catch(error => { success: true,
res.status(401).send({ success: false, message: error.message }); message: "Welcome to Seasoned!"
}); });
})
.catch(error => {
res.status(401).send({ success: false, message: error.message });
});
} }
module.exports = registerController; module.exports = registerController;

View File

@@ -1,47 +1,52 @@
const configuration = require('src/config/configuration').getInstance(); const configuration = require("src/config/configuration").getInstance();
const Tautulli = require('src/tautulli/tautulli'); const Tautulli = require("src/tautulli/tautulli");
const apiKey = configuration.get('tautulli', 'apiKey'); const apiKey = configuration.get("tautulli", "apiKey");
const ip = configuration.get('tautulli', 'ip'); const ip = configuration.get("tautulli", "ip");
const port = configuration.get('tautulli', 'port'); const port = configuration.get("tautulli", "port");
const tautulli = new Tautulli(apiKey, ip, port); const tautulli = new Tautulli(apiKey, ip, port);
function handleError(error, res) { function handleError(error, res) {
const { status, message } = error; const { status, message } = error;
if (status && message) { if (status && message) {
res.status(status).send({ success: false, message }) return res.status(status).send({ success: false, message });
} else { } else {
console.log('caught view history controller error', error) console.log("caught view history controller error", error);
res.status(500).send({ message: 'An unexpected error occured while fetching view history'}) return res.status(500).send({
message: "An unexpected error occured while fetching view history"
});
} }
} }
function watchTimeStatsController(req, res) { function watchTimeStatsController(req, res) {
const user = req.loggedInUser; const user = req.loggedInUser;
tautulli.watchTimeStats(user.plex_userid) return tautulli
.watchTimeStats(user.plex_userid)
.then(data => { .then(data => {
console.log('data', data, JSON.stringify(data.response.data))
return res.send({ return res.send({
success: true, success: true,
data: data.response.data, 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) { function getPlaysByDayOfWeekController(req, res) {
const user = req.loggedInUser; const user = req.loggedInUser;
const { days, y_axis } = req.query; const { days, y_axis } = req.query;
tautulli.getPlaysByDayOfWeek(user.plex_userid, days, y_axis) return tautulli
.then(data => res.send({ .getPlaysByDayOfWeek(user.plex_userid, days, y_axis)
success: true, .then(data =>
data: data.response.data, res.send({
message: 'play by day of week successfully fetched from tautulli' 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) { function getPlaysByDaysController(req, res) {
@@ -52,49 +57,46 @@ function getPlaysByDaysController(req, res) {
return res.status(422).send({ return res.status(422).send({
success: false, success: false,
message: "Missing parameter: days (number)" message: "Missing parameter: days (number)"
}) });
} }
const allowedYAxisDataType = ['plays', 'duration']; const allowedYAxisDataType = ["plays", "duration"];
if (!allowedYAxisDataType.includes(y_axis)) { if (!allowedYAxisDataType.includes(y_axis)) {
return res.status(422).send({ return res.status(422).send({
success: false, 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) return tautulli
.then(data => res.send({ .getPlaysByDays(user.plex_userid, days, y_axis)
.then(data =>
res.send({
success: true, success: true,
data: data.response.data data: data.response.data
})) })
)
.catch(error => handleError(error, res));
} }
function userViewHistoryController(req, 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 // const username = user.username;
// 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 = { module.exports = {

View File

@@ -1,11 +1,11 @@
const mustBeAuthenticated = (req, res, next) => { 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,
message: 'You must be logged in.', message: "You must be logged in."
}); });
} }
return next(); return next();
}; };
module.exports = mustBeAuthenticated; module.exports = mustBeAuthenticated;

View File

@@ -0,0 +1,32 @@
/* eslint-disable no-param-reassign */
const configuration = require("src/config/configuration").getInstance();
const Token = require("src/user/token");
const secret = configuration.get("authentication", "secret");
// Token example:
// curl -i -H "Authorization:[token]" localhost:31459/api/v1/user/history
const reqTokenToUser = (req, res, next) => {
const cookieAuthToken = req.cookies.authorization;
const headerAuthToken = req.headers.authorization;
if (cookieAuthToken || headerAuthToken) {
try {
const token = Token.fromString(
cookieAuthToken || headerAuthToken,
secret
);
req.loggedInUser = token.user;
} catch (error) {
req.loggedInUser = undefined;
}
} else {
// guest session
console.debug("No auth token in header or cookie.");
}
next();
};
module.exports = reqTokenToUser;

View File

@@ -1,23 +0,0 @@
/* eslint-disable no-param-reassign */
const configuration = require('src/config/configuration').getInstance();
const secret = configuration.get('authentication', 'secret');
const Token = require('src/user/token');
// Token example:
// curl -i -H "Authorization:[token]" localhost:31459/api/v1/user/history
const tokenToUser = (req, res, next) => {
const rawToken = req.headers.authorization;
if (rawToken) {
try {
const token = Token.fromString(rawToken, secret);
req.loggedInUser = token.user;
} catch (error) {
req.loggedInUser = undefined;
}
}
next();
};
module.exports = tokenToUser;

File diff suppressed because it is too large Load Diff