Compare commits
	
		
			2 Commits
		
	
	
		
			v1.1.1
			...
			research/g
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 488da889d8 | |||
| 8da7c159b1 | 
| @@ -11,11 +11,6 @@ | ||||
| 	"plex": { | ||||
| 		"ip": "" | ||||
| 	}, | ||||
|   "tautulli": { | ||||
|     "apiKey": "", | ||||
|     "ip": "", | ||||
|     "port": "" | ||||
|   }, | ||||
| 	"raven": { | ||||
| 		"DSN": "" | ||||
| 	}, | ||||
|   | ||||
| @@ -22,7 +22,9 @@ | ||||
|     "body-parser": "~1.18.2", | ||||
|     "cross-env": "~5.1.4", | ||||
|     "express": "~4.16.0", | ||||
|     "form-data": "^2.5.1", | ||||
|     "express-graphql": "^0.9.0", | ||||
|     "express-reload": "^1.2.0", | ||||
|     "graphql": "^14.5.8", | ||||
|     "jsonwebtoken": "^8.2.0", | ||||
|     "km-moviedb": "^0.2.12", | ||||
|     "node-cache": "^4.1.1", | ||||
|   | ||||
| @@ -1,19 +1,11 @@ | ||||
| CREATE TABLE IF NOT EXISTS user ( | ||||
|     user_name varchar(127) UNIQUE, | ||||
|     password varchar(127), | ||||
|     admin boolean DEFAULT 0, | ||||
|     email varchar(127) UNIQUE, | ||||
|     admin boolean DEFAULT 0, | ||||
|     primary key (user_name) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE IF NOT EXISTS settings ( | ||||
|     user_name varchar(127) UNIQUE, | ||||
|     dark_mode boolean DEFAULT 0, | ||||
|     plex_userid varchar(127) DEFAULT NULL, | ||||
|     emoji varchar(16) DEFAULT NULL, | ||||
|     foreign key(user_name) REFERENCES user(user_name) ON DELETE CASCADE | ||||
| ); | ||||
|  | ||||
| CREATE TABLE IF NOT EXISTS cache ( | ||||
|     key varchar(255), | ||||
|     value blob, | ||||
| @@ -36,13 +28,12 @@ CREATE TABLE IF NOT EXISTS requests( | ||||
|     year NUMBER, | ||||
|     poster_path TEXT DEFAULT NULL, | ||||
|     background_path TEXT DEFAULT NULL, | ||||
|     requested_by varchar(127) DEFAULT NULL, | ||||
|     requested_by TEXT, | ||||
|     ip TEXT, | ||||
|     date DATE DEFAULT CURRENT_TIMESTAMP, | ||||
|     status CHAR(25) DEFAULT 'requested' NOT NULL, | ||||
|     user_agent CHAR(255) DEFAULT NULL, | ||||
|     type CHAR(50) DEFAULT 'movie', | ||||
|     foreign key(requested_by) REFERENCES user(user_name) ON DELETE SET NULL | ||||
|     type CHAR(50) DEFAULT 'movie' | ||||
| ); | ||||
|  | ||||
| CREATE TABLE IF NOT EXISTS request( | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| DROP TABLE IF EXISTS user; | ||||
| DROP TABLE IF EXISTS settings; | ||||
| DROP TABLE IF EXISTS search_history; | ||||
| DROP TABLE IF EXISTS requests; | ||||
| DROP TABLE IF EXISTS request; | ||||
|   | ||||
| @@ -6,7 +6,6 @@ class SqliteDatabase { | ||||
|    constructor(host) { | ||||
|       this.host = host; | ||||
|       this.connection = new sqlite3.Database(this.host); | ||||
|       this.execute('pragma foreign_keys = on;'); | ||||
|       this.schemaDirectory = path.join(__dirname, 'schemas'); | ||||
|    } | ||||
|  | ||||
|   | ||||
| @@ -86,18 +86,18 @@ class RequestRepository { | ||||
|    * @param {tmdb} tmdb class of movie|show to add | ||||
|    * @returns {Promise} | ||||
|    */ | ||||
|   requestFromTmdb(tmdb, ip, user_agent, username) { | ||||
|   requestFromTmdb(tmdb, ip, user_agent, user) { | ||||
|     return Promise.resolve() | ||||
|       .then(() => this.database.get(this.queries.read, [tmdb.id, tmdb.type])) | ||||
|       .then(row => assert.equal(row, undefined, 'Id has already been requested')) | ||||
|       .then(() => this.database.run(this.queries.add, [tmdb.id, tmdb.title, tmdb.year, tmdb.poster, tmdb.backdrop, username, ip, user_agent, tmdb.type])) | ||||
|       .catch((error) => { | ||||
|         if (error.name === 'AssertionError' || error.message.endsWith('been requested')) { | ||||
|           throw new Error('This id is already requested', error.message); | ||||
|         } | ||||
|         console.log('Error @ request.addTmdb:', error); | ||||
|         throw new Error('Could not add request'); | ||||
|       }); | ||||
|     .then(() => this.database.get(this.queries.read, [tmdb.id, tmdb.type])) | ||||
|     .then(row => assert.equal(row, undefined, 'Id has already been requested')) | ||||
|     .then(() => this.database.run(this.queries.add, [tmdb.id, tmdb.title, tmdb.year, tmdb.poster, tmdb.backdrop, user, ip, user_agent, tmdb.type])) | ||||
|     .catch((error) => { | ||||
|       if (error.name === 'AssertionError' || error.message.endsWith('been requested')) { | ||||
|         throw new Error('This id is already requested', error.message); | ||||
|       } | ||||
|       console.log('Error @ request.addTmdb:', error); | ||||
|       throw new Error('Could not add request'); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|   | ||||
| @@ -28,23 +28,17 @@ class SearchHistory { | ||||
|  | ||||
|    /** | ||||
|    * Creates a new search entry in the database. | ||||
|    * @param {String} username logged in user doing the search | ||||
|    * @param {User} user a new user | ||||
|    * @param {String} searchQuery the query the user searched for | ||||
|    * @returns {Promise} | ||||
|    */ | ||||
|    create(username, searchQuery) { | ||||
|       return this.database.run(this.queries.create, [searchQuery, username]) | ||||
|          .catch(error => { | ||||
|    create(user, searchQuery) { | ||||
|       return Promise.resolve() | ||||
|          .then(() => this.database.run(this.queries.create, [searchQuery, user])) | ||||
|          .catch((error) => { | ||||
|             if (error.message.includes('FOREIGN')) { | ||||
|                throw new Error('Could not create search history.'); | ||||
|             } | ||||
|  | ||||
|             throw { | ||||
|                success: false, | ||||
|                status: 500, | ||||
|                message: 'An unexpected error occured', | ||||
|                source: 'database' | ||||
|             } | ||||
|          }); | ||||
|    } | ||||
| } | ||||
|   | ||||
| @@ -1,58 +0,0 @@ | ||||
| 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,218 +2,64 @@ 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 = ?' | ||||
|     }; | ||||
|   } | ||||
|    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 = ?' | ||||
|       }; | ||||
|    } | ||||
|  | ||||
|   /** | ||||
|   * Create a user in a database. | ||||
|   * @param {User} user the user you want to create | ||||
|   * @returns {Promise} | ||||
|   */ | ||||
|   create(user) { | ||||
|     return this.database.get(this.queries.read, user.username) | ||||
|       .then(() => this.database.run(this.queries.create, user.username)) | ||||
|       .catch((error) => { | ||||
|         if (error.name === 'AssertionError' || error.message.endsWith('user_name')) { | ||||
|            throw new Error('That username is already registered'); | ||||
|         } | ||||
|         throw Error(error) | ||||
|       }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|   * Retrieve a password from a database. | ||||
|   * @param {User} user the user you want to retrieve the password | ||||
|   * @returns {Promise} | ||||
|   */ | ||||
|   retrieveHash(user) { | ||||
|     return this.database.get(this.queries.retrieveHash, user.username) | ||||
|       .then(row => { | ||||
|         assert(row, 'The user does not exist.'); | ||||
|         return row.password; | ||||
|       }) | ||||
|       .catch(err => { console.log(error); throw new Error('Unable to find your user.'); }) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|   * Change a user's password in a database. | ||||
|   * @param {User} user the user you want to create | ||||
|   * @param {String} password the new password you want to change | ||||
|   * @returns {Promise} | ||||
|   */ | ||||
|   changePassword(user, password) { | ||||
|     return this.database.run(this.queries.change, [password, user.username]) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|   * Link plex userid with seasoned user | ||||
|   * @param {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' | ||||
|               }) | ||||
|    /** | ||||
|    * Create a user in a database. | ||||
|    * @param {User} user the user you want to create | ||||
|    * @returns {Promise} | ||||
|    */ | ||||
|    create(user) { | ||||
|       return Promise.resolve() | ||||
|          .then(() => this.database.get(this.queries.read, user.username)) | ||||
|          .then(() => this.database.run(this.queries.create, user.username)) | ||||
|          .catch((error) => { | ||||
|             if (error.name === 'AssertionError' || error.message.endsWith('user_name')) { | ||||
|                throw new Error('That username is already registered'); | ||||
|             } | ||||
|           } | ||||
|             throw Error(error) | ||||
|          }); | ||||
|    } | ||||
|  | ||||
|           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' | ||||
|           }) | ||||
|         }) | ||||
|     }) | ||||
|   } | ||||
|    /** | ||||
|    * 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.'); }); | ||||
|    } | ||||
|  | ||||
|  /** | ||||
|   * 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 | ||||
|    /** | ||||
|    * 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])); | ||||
|    } | ||||
|  | ||||
|     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) | ||||
|    checkAdmin(user) { | ||||
|       return this.database.get(this.queries.getAdminStateByUser, user.username).then((row) => { | ||||
|          return row.admin; | ||||
|       }); | ||||
|    } | ||||
| } | ||||
|  | ||||
| module.exports = UserRepository; | ||||
|   | ||||
| @@ -1,16 +1,13 @@ | ||||
| const express = require('express'); | ||||
| // const reload = require('express-reload'); | ||||
| const Raven = require('raven'); | ||||
| const bodyParser = require('body-parser'); | ||||
| const tokenToUser = require('./middleware/tokenToUser'); | ||||
| const mustBeAuthenticated = require('./middleware/mustBeAuthenticated'); | ||||
| const mustBeAdmin = require('./middleware/mustBeAdmin'); | ||||
| const mustHaveAccountLinkedToPlex = require('./middleware/mustHaveAccountLinkedToPlex'); | ||||
| const configuration = require('src/config/configuration').getInstance(); | ||||
|  | ||||
| const listController = require('./controllers/list/listController'); | ||||
| const tautulli = require('./controllers/user/viewHistory.js'); | ||||
| const SettingsController = require('./controllers/user/settings'); | ||||
| const AuthenticatePlexAccountController = require('./controllers/user/AuthenticatePlexAccount'); | ||||
|  | ||||
| // TODO: Have our raven router check if there is a value, if not don't enable raven. | ||||
| Raven.config(configuration.get('raven', 'DSN')).install(); | ||||
| @@ -27,7 +24,7 @@ const allowedOrigins = ['https://kevinmidboe.com', 'http://localhost:8080']; | ||||
| app.use(bodyParser.urlencoded({ extended: true })); | ||||
|  | ||||
| /* Decode the Authorization header if provided */ | ||||
| router.use(tokenToUser); | ||||
| app.use(tokenToUser); | ||||
|  | ||||
| // TODO: Should have a separate middleware/router for handling headers. | ||||
| router.use((req, res, next) => { | ||||
| @@ -52,22 +49,39 @@ app.use(function onError(err, req, res, next) { | ||||
|    res.end(res.sentry + '\n'); | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * GraphQL | ||||
|  */ | ||||
| var graphqlHTTP = require('express-graphql'); | ||||
| var { buildSchema } = require('graphql'); | ||||
|  | ||||
| // var schema = buildSchema(` | ||||
| //   type Query { | ||||
| //     hello: String | ||||
| //   }`); | ||||
| const schema = require('./graphql/requests.js'); | ||||
|  | ||||
| const roots = { hello: () => 'Hello world!' }; | ||||
|  | ||||
| app.use('/graphql', graphqlHTTP({ | ||||
|   schema: schema.schema, | ||||
|   graphiql: true | ||||
| })) | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * User | ||||
|  */ | ||||
| router.post('/v1/user', require('./controllers/user/register.js')); | ||||
| router.post('/v1/user/login', require('./controllers/user/login.js')); | ||||
| router.get('/v1/user/settings', mustBeAuthenticated, SettingsController.getSettingsController); | ||||
| router.put('/v1/user/settings', mustBeAuthenticated, SettingsController.updateSettingsController); | ||||
| router.get('/v1/user/search_history', mustBeAuthenticated, require('./controllers/user/searchHistory.js')); | ||||
| router.get('/v1/user/history', mustBeAuthenticated, require('./controllers/user/history.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 | ||||
|   | ||||
| @@ -24,7 +24,7 @@ function multiSearchController(req, res) { | ||||
|   const { query, page } = req.query; | ||||
|  | ||||
|   if (user) { | ||||
|     searchHistory.create(user.username, query) | ||||
|     searchHistory.create(user, query) | ||||
|   } | ||||
|  | ||||
|   return tmdb.multiSearch(query, page) | ||||
|   | ||||
| @@ -1,87 +0,0 @@ | ||||
| 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,9 +8,6 @@ const secret = configuration.get('authentication', 'secret'); | ||||
| const userSecurity = new UserSecurity(); | ||||
| const userRepository = new UserRepository(); | ||||
|  | ||||
| // TODO look to move some of the token generation out of the reach of the final "catch-all" | ||||
| // catch including the, maybe sensitive, error message. | ||||
|  | ||||
| /** | ||||
|  * Controller: Log in a user provided correct credentials. | ||||
|  * @param {Request} req http request variable | ||||
|   | ||||
| @@ -1,42 +0,0 @@ | ||||
| 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 | ||||
| } | ||||
| @@ -1,105 +0,0 @@ | ||||
| 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 | ||||
| }; | ||||
							
								
								
									
										294
									
								
								seasoned_api/src/webserver/graphql/requests.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										294
									
								
								seasoned_api/src/webserver/graphql/requests.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,294 @@ | ||||
| const graphql = require("graphql"); | ||||
| const establishedDatabase = require('src/database/database'); | ||||
| const fetch = require('node-fetch'); | ||||
|  | ||||
|  | ||||
| const TorrentType = new graphql.GraphQLObjectType({ | ||||
|   name: "Torrent", | ||||
|   fields: { | ||||
|     magnet: { type: graphql.GraphQLString }, | ||||
|     torrent_name: { type: graphql.GraphQLString}, | ||||
|     tmdb_id: { type: graphql.GraphQLString } | ||||
|   } | ||||
| }); | ||||
|  | ||||
| const RequestType = new graphql.GraphQLObjectType({ | ||||
|   name: "Request", | ||||
|   fields: { | ||||
|     id: { type: graphql.GraphQLID }, | ||||
|     title: { type: graphql.GraphQLString }, | ||||
|     year: { type: graphql.GraphQLInt}, | ||||
|     poster_path: { type: graphql.GraphQLString }, | ||||
|     background_path: { type: graphql.GraphQLString }, | ||||
|     requested_by: { type: graphql.GraphQLString }, | ||||
|     ip: { type: graphql.GraphQLString }, | ||||
|     date: { type: graphql.GraphQLString }, | ||||
|     status: { type: graphql.GraphQLString }, | ||||
|     user_agent: { type: graphql.GraphQLString }, | ||||
|     type: { type: graphql.GraphQLString } | ||||
|   } | ||||
| }); | ||||
|  | ||||
| const RequestsType = new graphql.GraphQLObjectType({ | ||||
|   name: 'Requests', | ||||
|   type: graphql.GraphQLList(RequestType), | ||||
|   resolve: (root, args, context, info) => { | ||||
|     return establishedDatabase.all("SELECT * FROM requests;") | ||||
|       .catch(error => console.error("something went wrong fetching 'all' query. Error:", error)) | ||||
|   } | ||||
| }) | ||||
|  | ||||
| const ProgressType = new graphql.GraphQLObjectType({ | ||||
|   name: 'TorrentProgress', | ||||
|   fields: { | ||||
|     eta: { type: graphql.GraphQLInt }, | ||||
|     finished: { type: graphql.GraphQLBoolean }, | ||||
|     key: { type: graphql.GraphQLString }, | ||||
|     name: { type: graphql.GraphQLString }, | ||||
|     progress: { type: graphql.GraphQLFloat }, | ||||
|     state: { type: graphql.GraphQLString }, | ||||
|     Torrent: { | ||||
|       type: TorrentType, | ||||
|       resolve(parent) { | ||||
|         console.log('prante: ', parent.name) | ||||
|         console.log(parent.name.slice(0,10)) | ||||
|         return establishedDatabase.get(`select magnet, torrent_name, tmdb_id from requested_torrent where torrent_name like (?);`, [parent.name.slice(0, 10) + '%']) | ||||
|       } | ||||
|     }, | ||||
|     Requested: { | ||||
|       type: graphql.GraphQLList(RequestType), | ||||
|       // resolve: () => fetch('https://api.kevinmidboe.com/api/v2/request?page=1/').then(resp => resp.json()) | ||||
|       // .then(data => { | ||||
|       //   // console.log('data', data) | ||||
|       //   return data.results | ||||
|       // }) | ||||
|       resolve: (parent) => { | ||||
|         return establishedDatabase.all("SELECT * FROM requests;") | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| }) | ||||
|  | ||||
|  | ||||
| const TorrentsRequestedType = new graphql.GraphQLObjectType({ | ||||
|   name: 'TorrentsRequested', | ||||
|   fields: { | ||||
|     magnet: { type: graphql.GraphQLString }, | ||||
|     torrent_name: { type: graphql.GraphQLString }, | ||||
|     tmdb_id: { type: graphql.GraphQLString }, | ||||
|     date_added: { type: graphql.GraphQLString }, | ||||
|     Request: { | ||||
|       type: RequestType, | ||||
|       // resolve: () => fetch('https://api.kevinmidboe.com/api/v2/request?page=1/').then(resp => resp.json()) | ||||
|       // .then(data => { | ||||
|       //   return data.results | ||||
|       // }) | ||||
|       resolve(parentValue, args) { | ||||
|         return establishedDatabase.get('select * from requests where id = (?);', [parentValue.tmdb_id]) | ||||
|       } | ||||
|     }, | ||||
|     Progress: { | ||||
|       type: ProgressType, | ||||
|       resolve(parentValue, args) { | ||||
|         return fetch('http://localhost:5000/') | ||||
|           .then(resp => resp.json()) | ||||
|           // .then(data => { console.log('data', data); return data.filter(download => download.name === parentValue.torrent_name) }) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| }) | ||||
|  | ||||
|  | ||||
| // create a graphql query to select all and by id | ||||
| var queryType = new graphql.GraphQLObjectType({ | ||||
|     name: 'Query', | ||||
|     fields: { | ||||
|         //first query to select all | ||||
|         Requests: { | ||||
|             type: graphql.GraphQLList(RequestType), | ||||
|             resolve: (root, args, context, info) => { | ||||
|                 return establishedDatabase.all("SELECT * FROM requests;") | ||||
|                   .catch(error => console.error("something went wrong fetching 'all' query. Error:", error)) | ||||
|             } | ||||
|         }, | ||||
|         Progress: { | ||||
|           type: graphql.GraphQLList(ProgressType), | ||||
|           resolve: (root, args, context, info) => { | ||||
|             console.log('user', context.loggedInUser) | ||||
|             return fetch('http://localhost:5000') | ||||
|               .then(resp => resp.json()) | ||||
|           } | ||||
|         }, | ||||
|         ProgressRequested: { | ||||
|           type: graphql.GraphQLList(ProgressType), | ||||
|           resolve: (root, args, context, info) => { | ||||
|             console.log('root & args', root, args) | ||||
|           } | ||||
|         }, | ||||
|         TorrentsRequested: { | ||||
|           type: graphql.GraphQLList(TorrentsRequestedType), | ||||
|           resolve: (root, args, context, info) => { | ||||
|             return establishedDatabase.all("SELECT * FROM requested_torrent;") | ||||
|               .then(data => data.filter(request => { if (request.tmdb_id === '83666') { console.log('request', request, root);}; return request })) | ||||
|               .catch(error => console.error("something went wrong fetching 'all' query. Error:", error)) | ||||
|           } | ||||
|         }, | ||||
|         DownloadingRequestByName: { | ||||
|           type: RequestType, | ||||
|           args:{ | ||||
|             name:{ | ||||
|               type: new graphql.GraphQLNonNull(graphql.GraphQLString) | ||||
|             }                | ||||
|           }, | ||||
|           resolve: (root, { name }, context, info) => { | ||||
|             return establishedDatabase.all("SELECT * FROM requests where requested_by = (?);", [name]) | ||||
|               .catch(error => console.error("something went wrong fetching 'all' query. Error:", error)) | ||||
|           } | ||||
|         }, | ||||
|         //second query to select by id | ||||
|         Request:{ | ||||
|             type: RequestType, | ||||
|             args:{ | ||||
|                 id:{ | ||||
|                     type: new graphql.GraphQLNonNull(graphql.GraphQLID) | ||||
|                 }                | ||||
|             }, | ||||
|             resolve: (root, {id}, context, info) => { | ||||
|               return establishedDatabase.get("SELECT * FROM requests WHERE id = (?);",[id]) | ||||
|                 .catch(error => console.error(`something went wrong fetching by id: '${ id }'. Error: ${ error }`)) | ||||
|             } | ||||
|         }, | ||||
|         Torrents: { | ||||
|           type: graphql.GraphQLList(TorrentType), | ||||
|           resolve: (root, {id}, context, info) => { | ||||
|             // console.log('parent', parent) | ||||
|             return establishedDatabase.all("SELECT * FROM requested_torrent") | ||||
|               .catch(error => console.error(`something went wrong fetching all torrents. Error: ${ error }`)) | ||||
|           } | ||||
|         }, | ||||
|         Torrent: { | ||||
|           type: TorrentType, | ||||
|           args: { | ||||
|             id: { | ||||
|               type: new graphql.GraphQLNonNull(graphql.GraphQLID) | ||||
|             } | ||||
|           }, | ||||
|           resolve: (parent, {id}, context, info) => { | ||||
|             // console.log('searcing from parent', parent) | ||||
|             return establishedDatabase.get("SELECT * FROM requested_torrent WHERE tmdb_id = (?);", [id]) | ||||
|           } | ||||
|         } | ||||
|     } | ||||
| }); | ||||
| //mutation type is a type of object to modify data (INSERT,DELETE,UPDATE) | ||||
| // var mutationType = new graphql.GraphQLObjectType({ | ||||
| //     name: 'Mutation', | ||||
| //     fields: { | ||||
| //       //mutation for creacte | ||||
| //       createPost: { | ||||
| //         //type of object to return after create in SQLite | ||||
| //         type: PostType, | ||||
| //         //argument of mutation creactePost to get from request | ||||
| //         args: { | ||||
| //           title: { | ||||
| //             type: new graphql.GraphQLNonNull(graphql.GraphQLString) | ||||
| //           }, | ||||
| //           description:{ | ||||
| //               type: new graphql.GraphQLNonNull(graphql.GraphQLString) | ||||
| //           }, | ||||
| //           createDate:{ | ||||
| //               type: new graphql.GraphQLNonNull(graphql.GraphQLString) | ||||
| //           }, | ||||
| //           author:{ | ||||
| //               type: new graphql.GraphQLNonNull(graphql.GraphQLString) | ||||
| //           } | ||||
| //         }, | ||||
| //         resolve: (root, {title, description, createDate, author}) => { | ||||
| //             return new Promise((resolve, reject) => { | ||||
| //                 //raw SQLite to insert a new post in post table | ||||
| //                 database.run('INSERT INTO Posts (title, description, createDate, author) VALUES (?,?,?,?);', [title, description, createDate, author], (err) => { | ||||
| //                     if(err) { | ||||
| //                         reject(null); | ||||
| //                     } | ||||
| //                     database.get("SELECT last_insert_rowid() as id", (err, row) => { | ||||
|                          | ||||
| //                         resolve({ | ||||
| //                             id: row["id"], | ||||
| //                             title: title, | ||||
| //                             description: description, | ||||
| //                             createDate:createDate, | ||||
| //                             author: author | ||||
| //                         }); | ||||
| //                     }); | ||||
| //                 }); | ||||
| //             }) | ||||
| //         } | ||||
| //       }, | ||||
| //       //mutation for update | ||||
| //       updatePost: { | ||||
| //         //type of object to return afater update in SQLite | ||||
| //         type: graphql.GraphQLString, | ||||
| //         //argument of mutation creactePost to get from request | ||||
| //         args:{ | ||||
| //             id:{ | ||||
| //                 type: new graphql.GraphQLNonNull(graphql.GraphQLID) | ||||
| //             }, | ||||
| //             title: { | ||||
| //                 type: new graphql.GraphQLNonNull(graphql.GraphQLString) | ||||
| //             }, | ||||
| //             description:{ | ||||
| //                   type: new graphql.GraphQLNonNull(graphql.GraphQLString) | ||||
| //             }, | ||||
| //             createDate:{ | ||||
| //                   type: new graphql.GraphQLNonNull(graphql.GraphQLString) | ||||
| //             }, | ||||
| //             author:{ | ||||
| //                   type: new graphql.GraphQLNonNull(graphql.GraphQLString) | ||||
| //             }              | ||||
| //         }, | ||||
| //         resolve: (root, {id, title, description, createDate, author}) => { | ||||
| //             return new Promise((resolve, reject) => { | ||||
| //                 //raw SQLite to update a post in post table | ||||
| //                 database.run('UPDATE Posts SET title = (?), description = (?), createDate = (?), author = (?) WHERE id = (?);', [title, description, createDate, author, id], (err) => { | ||||
| //                     if(err) { | ||||
| //                         reject(err); | ||||
| //                     } | ||||
| //                     resolve(`Post #${id} updated`); | ||||
| //                 }); | ||||
| //             }) | ||||
| //         } | ||||
| //       }, | ||||
| //       //mutation for update | ||||
| //       deletePost: { | ||||
| //          //type of object resturn after delete in SQLite | ||||
| //         type: graphql.GraphQLString, | ||||
| //         args:{ | ||||
| //             id:{ | ||||
| //                 type: new graphql.GraphQLNonNull(graphql.GraphQLID) | ||||
| //             }                | ||||
| //         }, | ||||
| //         resolve: (root, {id}) => { | ||||
| //             return new Promise((resolve, reject) => { | ||||
| //                 //raw query to delete from post table by id | ||||
| //                 database.run('DELETE from Posts WHERE id =(?);', [id], (err) => { | ||||
| //                     if(err) { | ||||
| //                         reject(err); | ||||
| //                     } | ||||
| //                     resolve(`Post #${id} deleted`);                     | ||||
| //                 }); | ||||
| //             }) | ||||
| //         } | ||||
| //       } | ||||
| //     } | ||||
| // }); | ||||
|  | ||||
| //define schema with post object, queries, and mustation  | ||||
| const schema = new graphql.GraphQLSchema({ | ||||
|     query: queryType, | ||||
|     // mutation: mutationType  | ||||
| }); | ||||
|  | ||||
| //export schema to use on index.js | ||||
| module.exports = { | ||||
|     schema | ||||
| } | ||||
| @@ -6,7 +6,7 @@ const mustBeAdmin = (req, res, next) => { | ||||
|    if (req.loggedInUser === undefined) { | ||||
|       return res.status(401).send({ | ||||
|          success: false, | ||||
|          message: 'You must be logged in.', | ||||
|          error: 'You must be logged in.', | ||||
|       }); | ||||
|    } else { | ||||
|       database.get(`SELECT admin FROM user WHERE user_name IS ?`, req.loggedInUser.username) | ||||
| @@ -15,7 +15,7 @@ const mustBeAdmin = (req, res, next) => { | ||||
|          if (isAdmin.admin == 0) { | ||||
|             return res.status(401).send({ | ||||
|                success: false, | ||||
|                message: 'You must be logged in as a admin.' | ||||
|                error: 'You must be logged in as a admin.' | ||||
|             }) | ||||
|          } | ||||
|       }) | ||||
|   | ||||
| @@ -2,7 +2,7 @@ const mustBeAuthenticated = (req, res, next) => { | ||||
|    if (req.loggedInUser === undefined) { | ||||
|       return res.status(401).send({ | ||||
|          success: false, | ||||
|          message: 'You must be logged in.', | ||||
|          error: 'You must be logged in.', | ||||
|       }); | ||||
|    } | ||||
|    return next(); | ||||
|   | ||||
| @@ -1,31 +0,0 @@ | ||||
| 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; | ||||
| @@ -989,7 +989,7 @@ abbrev@1.0.x: | ||||
|   resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" | ||||
|   integrity sha1-kbR5JYinc4wl813W9jdSovh3YTU= | ||||
|  | ||||
| accepts@~1.3.5: | ||||
| accepts@^1.3.7, accepts@~1.3.5: | ||||
|   version "1.3.7" | ||||
|   resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" | ||||
|   integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== | ||||
| @@ -1086,6 +1086,14 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: | ||||
|   dependencies: | ||||
|     color-convert "^1.9.0" | ||||
|  | ||||
| anymatch@^1.3.0: | ||||
|   version "1.3.2" | ||||
|   resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" | ||||
|   integrity sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA== | ||||
|   dependencies: | ||||
|     micromatch "^2.1.5" | ||||
|     normalize-path "^2.0.0" | ||||
|  | ||||
| anymatch@^2.0.0: | ||||
|   version "2.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" | ||||
| @@ -1200,7 +1208,7 @@ assign-symbols@^1.0.0: | ||||
|   resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" | ||||
|   integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= | ||||
|  | ||||
| async-each@^1.0.1: | ||||
| async-each@^1.0.0, async-each@^1.0.1: | ||||
|   version "1.0.3" | ||||
|   resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" | ||||
|   integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== | ||||
| @@ -1487,6 +1495,11 @@ bytes@3.0.0: | ||||
|   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" | ||||
|   integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= | ||||
|  | ||||
| bytes@3.1.0: | ||||
|   version "3.1.0" | ||||
|   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" | ||||
|   integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== | ||||
|  | ||||
| cache-base@^1.0.1: | ||||
|   version "1.0.1" | ||||
|   resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" | ||||
| @@ -1603,6 +1616,22 @@ charenc@~0.0.1: | ||||
|   resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" | ||||
|   integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= | ||||
|  | ||||
| chokidar@^1.7.0: | ||||
|   version "1.7.0" | ||||
|   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" | ||||
|   integrity sha1-eY5ol3gVHIB2tLNg5e3SjNortGg= | ||||
|   dependencies: | ||||
|     anymatch "^1.3.0" | ||||
|     async-each "^1.0.0" | ||||
|     glob-parent "^2.0.0" | ||||
|     inherits "^2.0.1" | ||||
|     is-binary-path "^1.0.0" | ||||
|     is-glob "^2.0.0" | ||||
|     path-is-absolute "^1.0.0" | ||||
|     readdirp "^2.0.0" | ||||
|   optionalDependencies: | ||||
|     fsevents "^1.0.0" | ||||
|  | ||||
| chokidar@^2.0.4: | ||||
|   version "2.1.8" | ||||
|   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" | ||||
| @@ -1797,7 +1826,7 @@ content-disposition@0.5.2: | ||||
|   resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" | ||||
|   integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ= | ||||
|  | ||||
| content-type@~1.0.4: | ||||
| content-type@^1.0.4, content-type@~1.0.4: | ||||
|   version "1.0.4" | ||||
|   resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" | ||||
|   integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== | ||||
| @@ -2503,6 +2532,24 @@ expand-range@^1.8.1: | ||||
|   dependencies: | ||||
|     fill-range "^2.1.0" | ||||
|  | ||||
| express-graphql@^0.9.0: | ||||
|   version "0.9.0" | ||||
|   resolved "https://registry.yarnpkg.com/express-graphql/-/express-graphql-0.9.0.tgz#00fd8552f866bac5c9a4612b2c4c82076107b3c2" | ||||
|   integrity sha512-wccd9Lb6oeJ8yHpUs/8LcnGjFUUQYmOG9A5BNLybRdCzGw0PeUrtBxsIR8bfiur6uSW4OvPkVDoYH06z6/N9+w== | ||||
|   dependencies: | ||||
|     accepts "^1.3.7" | ||||
|     content-type "^1.0.4" | ||||
|     http-errors "^1.7.3" | ||||
|     raw-body "^2.4.1" | ||||
|  | ||||
| express-reload@^1.2.0: | ||||
|   version "1.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/express-reload/-/express-reload-1.2.0.tgz#3c59734bc7508732e71e1d4e78cf116d582b676f" | ||||
|   integrity sha512-WS2xq7kOtspghADAzUSFMfHrqTtooXLeIg4Nxni5w/Qw5eOa5zic+T6glkWIs4oMCnnOq9d4k99g+bcbr+Z9bw== | ||||
|   dependencies: | ||||
|     chokidar "^1.7.0" | ||||
|     debug "^2.6.8" | ||||
|  | ||||
| express@~4.16.0: | ||||
|   version "4.16.4" | ||||
|   resolved "https://registry.yarnpkg.com/express/-/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e" | ||||
| @@ -2777,7 +2824,7 @@ forever-agent@~0.6.1: | ||||
|   resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" | ||||
|   integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= | ||||
|  | ||||
| form-data@^2.3.1, form-data@^2.5.1: | ||||
| form-data@^2.3.1: | ||||
|   version "2.5.1" | ||||
|   resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" | ||||
|   integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== | ||||
| @@ -2837,7 +2884,7 @@ fs.realpath@^1.0.0: | ||||
|   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" | ||||
|   integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= | ||||
|  | ||||
| fsevents@^1.2.7: | ||||
| fsevents@^1.0.0, fsevents@^1.2.7: | ||||
|   version "1.2.9" | ||||
|   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f" | ||||
|   integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw== | ||||
| @@ -3031,6 +3078,13 @@ graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6: | ||||
|   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" | ||||
|   integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== | ||||
|  | ||||
| graphql@^14.5.8: | ||||
|   version "14.5.8" | ||||
|   resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.8.tgz#504f3d3114cb9a0a3f359bbbcf38d9e5bf6a6b3c" | ||||
|   integrity sha512-MMwmi0zlVLQKLdGiMfWkgQD7dY/TUKt4L+zgJ/aR0Howebod3aNgP5JkgvAULiR2HPVZaP2VEElqtdidHweLkg== | ||||
|   dependencies: | ||||
|     iterall "^1.2.2" | ||||
|  | ||||
| growl@1.10.5, "growl@~> 1.10.0": | ||||
|   version "1.10.5" | ||||
|   resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" | ||||
| @@ -3195,6 +3249,17 @@ http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3: | ||||
|     setprototypeof "1.1.0" | ||||
|     statuses ">= 1.4.0 < 2" | ||||
|  | ||||
| http-errors@1.7.3, http-errors@^1.7.3: | ||||
|   version "1.7.3" | ||||
|   resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" | ||||
|   integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== | ||||
|   dependencies: | ||||
|     depd "~1.1.2" | ||||
|     inherits "2.0.4" | ||||
|     setprototypeof "1.1.1" | ||||
|     statuses ">= 1.5.0 < 2" | ||||
|     toidentifier "1.0.0" | ||||
|  | ||||
| "http-parser-js@>=0.4.0 <0.4.11": | ||||
|   version "0.4.10" | ||||
|   resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" | ||||
| @@ -3216,7 +3281,7 @@ iconv-lite@0.4.23: | ||||
|   dependencies: | ||||
|     safer-buffer ">= 2.1.2 < 3" | ||||
|  | ||||
| iconv-lite@^0.4.17, iconv-lite@^0.4.4: | ||||
| iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.4: | ||||
|   version "0.4.24" | ||||
|   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" | ||||
|   integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== | ||||
| @@ -3248,7 +3313,7 @@ inflight@^1.0.4: | ||||
|     once "^1.3.0" | ||||
|     wrappy "1" | ||||
|  | ||||
| inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: | ||||
| inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: | ||||
|   version "2.0.4" | ||||
|   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" | ||||
|   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== | ||||
| @@ -3724,6 +3789,11 @@ istanbul@^0.4.5: | ||||
|     which "^1.1.1" | ||||
|     wordwrap "^1.0.0" | ||||
|  | ||||
| iterall@^1.2.2: | ||||
|   version "1.2.2" | ||||
|   resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" | ||||
|   integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA== | ||||
|  | ||||
| js-levenshtein@^1.1.3: | ||||
|   version "1.1.6" | ||||
|   resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" | ||||
| @@ -3809,7 +3879,7 @@ jsonparse@^1.2.0: | ||||
|   resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" | ||||
|   integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= | ||||
|  | ||||
| jsonwebtoken@^8.0.1: | ||||
| jsonwebtoken@^8.2.0: | ||||
|   version "8.5.1" | ||||
|   resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" | ||||
|   integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== | ||||
| @@ -4207,7 +4277,7 @@ methods@^1.1.1, methods@^1.1.2, methods@~1.1.2: | ||||
|   resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" | ||||
|   integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= | ||||
|  | ||||
| micromatch@^2.3.11: | ||||
| micromatch@^2.1.5, micromatch@^2.3.11: | ||||
|   version "2.3.11" | ||||
|   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" | ||||
|   integrity sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU= | ||||
| @@ -4562,7 +4632,7 @@ normalize-package-data@^2.3.2: | ||||
|     semver "2 || 3 || 4 || 5" | ||||
|     validate-npm-package-license "^3.0.1" | ||||
|  | ||||
| normalize-path@^2.0.1, normalize-path@^2.1.1: | ||||
| normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1: | ||||
|   version "2.1.1" | ||||
|   resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" | ||||
|   integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= | ||||
| @@ -5259,6 +5329,16 @@ raw-body@2.3.3: | ||||
|     iconv-lite "0.4.23" | ||||
|     unpipe "1.0.0" | ||||
|  | ||||
| raw-body@^2.4.1: | ||||
|   version "2.4.1" | ||||
|   resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c" | ||||
|   integrity sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA== | ||||
|   dependencies: | ||||
|     bytes "3.1.0" | ||||
|     http-errors "1.7.3" | ||||
|     iconv-lite "0.4.24" | ||||
|     unpipe "1.0.0" | ||||
|  | ||||
| raw-body@~1.1.0: | ||||
|   version "1.1.7" | ||||
|   resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-1.1.7.tgz#1d027c2bfa116acc6623bca8f00016572a87d425" | ||||
| @@ -5366,7 +5446,7 @@ readable-stream@~2.1.0: | ||||
|     string_decoder "~0.10.x" | ||||
|     util-deprecate "~1.0.1" | ||||
|  | ||||
| readdirp@^2.2.1: | ||||
| readdirp@^2.0.0, readdirp@^2.2.1: | ||||
|   version "2.2.1" | ||||
|   resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" | ||||
|   integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== | ||||
| @@ -5593,7 +5673,7 @@ request-promise@^4.2: | ||||
|     stealthy-require "^1.1.1" | ||||
|     tough-cookie "^2.3.3" | ||||
|  | ||||
| request@^2.85.0, request@^2.86.0, request@^2.87.0: | ||||
| request@^2.86.0, request@^2.87.0: | ||||
|   version "2.88.0" | ||||
|   resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" | ||||
|   integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== | ||||
| @@ -5813,6 +5893,11 @@ setprototypeof@1.1.0: | ||||
|   resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" | ||||
|   integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== | ||||
|  | ||||
| setprototypeof@1.1.1: | ||||
|   version "1.1.1" | ||||
|   resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" | ||||
|   integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== | ||||
|  | ||||
| shebang-command@^1.2.0: | ||||
|   version "1.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" | ||||
| @@ -6017,7 +6102,7 @@ static-extend@^0.1.1: | ||||
|     define-property "^0.2.5" | ||||
|     object-copy "^0.1.0" | ||||
|  | ||||
| "statuses@>= 1.4.0 < 2": | ||||
| "statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2": | ||||
|   version "1.5.0" | ||||
|   resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" | ||||
|   integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= | ||||
| @@ -6402,6 +6487,11 @@ to-through@^2.0.0: | ||||
|   dependencies: | ||||
|     through2 "^2.0.3" | ||||
|  | ||||
| toidentifier@1.0.0: | ||||
|   version "1.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" | ||||
|   integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== | ||||
|  | ||||
| tough-cookie@^2.3.3: | ||||
|   version "2.5.0" | ||||
|   resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user