From 72f9624f1db721fe0324b7be9f0f811d2ae02389 Mon Sep 17 00:00:00 2001 From: sct Date: Tue, 18 Aug 2020 08:24:43 +0000 Subject: [PATCH] feat(plex/utils): added Plex OAuth class --- package.json | 5 +- pages/_app.tsx | 8 -- src/pages/_app.tsx | 12 ++ {pages => src/pages}/index.tsx | 0 src/pages/plextest.tsx | 39 ++++++ {styles => src/styles}/globals.css | 0 src/utils/plex.ts | 213 +++++++++++++++++++++++++++++ src/utils/typeHelpers.ts | 3 + tailwind.config.js | 7 +- tsconfig.json | 18 +-- yarn.lock | 62 ++++++++- 11 files changed, 342 insertions(+), 25 deletions(-) delete mode 100644 pages/_app.tsx create mode 100644 src/pages/_app.tsx rename {pages => src/pages}/index.tsx (100%) create mode 100644 src/pages/plextest.tsx rename {styles => src/styles}/globals.css (100%) create mode 100644 src/utils/plex.ts create mode 100644 src/utils/typeHelpers.ts diff --git a/package.json b/package.json index 3c0356a1..2e79a0d6 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,16 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "nodemon -e ts -x ts-node --project server/tsconfig.json server/index.ts", + "dev": "nodemon -e ts --watch server -x ts-node --project server/tsconfig.json server/index.ts", "build:server": "tsc --project server/tsconfig.json", "build:next": "next build", "build": "yarn build:next && yarn build:server", "start": "NODE_ENV=production node dist/server/index.js" }, "dependencies": { + "@tailwindcss/ui": "^0.5.0", + "axios": "^0.19.2", + "bowser": "^2.10.0", "express": "^4.17.1", "next": "9.5.2", "react": "16.13.1", diff --git a/pages/_app.tsx b/pages/_app.tsx deleted file mode 100644 index 9a5bf7ba..00000000 --- a/pages/_app.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; -import '../styles/globals.css'; - -function MyApp({ Component, pageProps }) { - return ; -} - -export default MyApp; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx new file mode 100644 index 00000000..f4aa3d9a --- /dev/null +++ b/src/pages/_app.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import '../styles/globals.css'; +import App from 'next/app'; + +class CoreApp extends App { + public render(): JSX.Element { + const { Component, pageProps } = this.props; + return ; + } +} + +export default CoreApp; diff --git a/pages/index.tsx b/src/pages/index.tsx similarity index 100% rename from pages/index.tsx rename to src/pages/index.tsx diff --git a/src/pages/plextest.tsx b/src/pages/plextest.tsx new file mode 100644 index 00000000..c710e0dd --- /dev/null +++ b/src/pages/plextest.tsx @@ -0,0 +1,39 @@ +import React, { useState } from 'react'; +import { NextPage } from 'next'; +import PlexOAuth from '../utils/plex'; + +const plexOAuth = new PlexOAuth(); + +const PlexText: NextPage = () => { + const [loading, setLoading] = useState(false); + const [authToken, setAuthToken] = useState(''); + + const getPlexLogin = async () => { + setLoading(true); + try { + const authToken = await plexOAuth.login(); + setAuthToken(authToken); + setLoading(false); + } catch (e) { + console.log(e.message); + setLoading(false); + } + }; + return ( +
+ + + +
Auth Token: {authToken}
+
+ ); +}; + +export default PlexText; diff --git a/styles/globals.css b/src/styles/globals.css similarity index 100% rename from styles/globals.css rename to src/styles/globals.css diff --git a/src/utils/plex.ts b/src/utils/plex.ts new file mode 100644 index 00000000..54ecab36 --- /dev/null +++ b/src/utils/plex.ts @@ -0,0 +1,213 @@ +import axios from 'axios'; +import Bowser from 'bowser'; + +interface PlexHeaders { + Accept: string; + 'X-Plex-Product': string; + 'X-Plex-Version': string; + 'X-Plex-Client-Identifier': string; + 'X-Plex-Model': string; + 'X-Plex-Platform': string; + 'X-Plex-Platform-Version': string; + 'X-Plex-Device': string; + 'X-Plex-Device-Name': string; + 'X-Plex-Device-Screen-Resolution': string; + 'X-Plex-Language': string; +} + +export interface PlexPin { + id: number; + code: string; +} + +class PlexOAuth { + private plexHeaders?: PlexHeaders; + + private pin?: PlexPin; + private popup?: Window; + + private authToken?: string; + + public initializeHeaders(): void { + if (!window) { + throw new Error( + 'Window is not defined. Are you calling this in the browser?' + ); + } + const browser = Bowser.getParser(window.navigator.userAgent); + this.plexHeaders = { + Accept: 'application/json', + 'X-Plex-Product': 'Overseerr', + 'X-Plex-Version': '2.0', + 'X-Plex-Client-Identifier': '7f9de3ba-e12b-11ea-87d0-0242ac130003', + 'X-Plex-Model': 'Plex OAuth', + 'X-Plex-Platform': browser.getOSName(), + 'X-Plex-Platform-Version': browser.getOSVersion(), + 'X-Plex-Device': browser.getBrowserName(), + 'X-Plex-Device-Name': browser.getBrowserVersion(), + 'X-Plex-Device-Screen-Resolution': + window.screen.width + 'x' + window.screen.height, + 'X-Plex-Language': 'en', + }; + } + + public async getPin(): Promise { + if (!this.plexHeaders) { + throw new Error( + 'You must initialize the plex headers clientside to login' + ); + } + try { + const response = await axios.post( + 'https://plex.tv/api/v2/pins?strong=true', + undefined, + { headers: this.plexHeaders } + ); + + this.pin = { id: response.data.id, code: response.data.code }; + + return this.pin; + } catch (e) { + throw e; + } + } + + public async login(): Promise { + try { + this.initializeHeaders(); + await this.getPin(); + + if (!this.plexHeaders || !this.pin) { + throw new Error('Unable to call login if class is not initialized.'); + } + + const params = { + clientID: this.plexHeaders['X-Plex-Client-Identifier'], + 'context[device][product]': this.plexHeaders['X-Plex-Product'], + 'context[device][version]': this.plexHeaders['X-Plex-Version'], + 'context[device][platform]': this.plexHeaders['X-Plex-Platform'], + 'context[device][platformVersion]': this.plexHeaders[ + 'X-Plex-Platform-Version' + ], + 'context[device][device]': this.plexHeaders['X-Plex-Device'], + 'context[device][deviceName]': this.plexHeaders['X-Plex-Device-Name'], + 'context[device][model]': this.plexHeaders['X-Plex-Model'], + 'context[device][screenResolution]': this.plexHeaders[ + 'X-Plex-Device-Screen-Resolution' + ], + 'context[device][layout]': 'desktop', + code: this.pin.code, + }; + this.openPopup({ + url: `https://app.plex.tv/auth/#!?${this.encodeData(params)}`, + title: 'Plex Auth', + w: 600, + h: 700, + }); + + return this.pinPoll(); + } catch (e) { + throw e; + } + } + + private async pinPoll(): Promise { + const executePoll = async ( + resolve: (authToken: string) => void, + reject: (e: Error) => void + ) => { + try { + if (!this.pin) { + throw new Error('Unable to poll when pin is not initialized.'); + } + + const response = await axios.get( + `https://plex.tv/api/v2/pins/${this.pin.id}`, + { headers: this.plexHeaders } + ); + + if (response.data?.authToken) { + this.authToken = response.data.authToken; + this.closePopup(); + resolve(response.data.authToken); + } else if (!response.data?.authToken && !this.popup?.closed) { + setTimeout(executePoll, 1000, resolve, reject); + } else { + reject(new Error('Popup closed without completing login')); + } + } catch (e) { + this.closePopup(); + reject(e); + } + }; + + return new Promise(executePoll); + } + + private closePopup(): void { + this.popup?.close(); + this.popup = undefined; + } + + private openPopup({ + url, + title, + w, + h, + }: { + url: string; + title: string; + w: number; + h: number; + }): Window | void { + if (!window) { + throw new Error( + 'Window is undefined. Are you running this in the browser?' + ); + } + // Fixes dual-screen position Most browsers Firefox + const dualScreenLeft = + window.screenLeft != undefined ? window.screenLeft : window.screenX; + const dualScreenTop = + window.screenTop != undefined ? window.screenTop : window.screenY; + const width = window.innerWidth + ? window.innerWidth + : document.documentElement.clientWidth + ? document.documentElement.clientWidth + : screen.width; + const height = window.innerHeight + ? window.innerHeight + : document.documentElement.clientHeight + ? document.documentElement.clientHeight + : screen.height; + const left = width / 2 - w / 2 + dualScreenLeft; + const top = height / 2 - h / 2 + dualScreenTop; + const newWindow = window.open( + url, + title, + 'scrollbars=yes, width=' + + w + + ', height=' + + h + + ', top=' + + top + + ', left=' + + left + ); + if (newWindow) { + newWindow.focus(); + this.popup = newWindow; + return this.popup; + } + } + + private encodeData(data: Record): string { + return Object.keys(data) + .map(function (key) { + return [key, data[key]].map(encodeURIComponent).join('='); + }) + .join('&'); + } +} + +export default PlexOAuth; diff --git a/src/utils/typeHelpers.ts b/src/utils/typeHelpers.ts new file mode 100644 index 00000000..2f47239c --- /dev/null +++ b/src/utils/typeHelpers.ts @@ -0,0 +1,3 @@ +export type Undefinable = T | undefined; +export type Nullable = T | null; +export type Maybe = T | null | undefined; diff --git a/tailwind.config.js b/tailwind.config.js index a684a3d7..4a591b4b 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,8 +1,13 @@ +/* eslint-disable */ module.exports = { purge: ['./pages/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'], theme: { extend: {}, }, variants: {}, - plugins: [], + plugins: [ + require('@tailwindcss/ui')({ + layout: 'sidebar', + }), + ], }; diff --git a/tsconfig.json b/tsconfig.json index 35d51eac..4fa631c2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,10 @@ { "compilerOptions": { "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, - "strict": false, + "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, @@ -18,12 +14,6 @@ "isolatedModules": true, "jsx": "preserve" }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx" - ], - "exclude": [ - "node_modules" - ] + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] } diff --git a/yarn.lock b/yarn.lock index 5877af11..0a5c08b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1218,6 +1218,30 @@ dependencies: defer-to-connect "^1.0.1" +"@tailwindcss/custom-forms@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@tailwindcss/custom-forms/-/custom-forms-0.2.1.tgz#40e5ed1fff6d29d8ed1c508a0b2aaf8da96962e0" + integrity sha512-XdP5XY6kxo3x5o50mWUyoYWxOPV16baagLoZ5uM41gh6IhXzhz/vJYzqrTb/lN58maGIKlpkxgVsQUNSsbAS3Q== + dependencies: + lodash "^4.17.11" + mini-svg-data-uri "^1.0.3" + traverse "^0.6.6" + +"@tailwindcss/typography@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@tailwindcss/typography/-/typography-0.2.0.tgz#b597c83502e3c3c6641a8aaabda223cd494ab349" + integrity sha512-aPgMH+CjQiScLZculoDNOQUrrK2ktkbl3D6uCLYp1jgYRlNDrMONu9nMu8LfwAeetYNpVNeIGx7WzHSu0kvECg== + +"@tailwindcss/ui@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@tailwindcss/ui/-/ui-0.5.0.tgz#c3b274222a57484757e664bc71c4f5288461c9ad" + integrity sha512-UbKe9ti0uMXN2lmgaFgNJC/DY4s2izLaowhIn2A4AgmplC2+XzcYJ9vHLLNNXNBthDq9X+js92tpxey6dBjgfw== + dependencies: + "@tailwindcss/custom-forms" "^0.2.1" + "@tailwindcss/typography" "^0.2.0" + hex-rgb "^4.1.0" + postcss-selector-parser "^6.0.2" + "@types/body-parser@*": version "1.19.0" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" @@ -1950,6 +1974,13 @@ axe-core@^3.5.4: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.5.tgz#84315073b53fa3c0c51676c588d59da09a192227" integrity sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q== +axios@^0.19.2: + version "0.19.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" + integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== + dependencies: + follow-redirects "1.5.10" + axobject-query@^2.1.2: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -2070,6 +2101,11 @@ body-parser@1.19.0: raw-body "2.4.0" type-is "~1.6.17" +bowser@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.10.0.tgz#be3736f161c4bb8b10958027ab99465d2a811198" + integrity sha512-OCsqTQboTEWWsUjcp5jLSw2ZHsBiv2C105iFs61bOT0Hnwi9p7/uuXdd7mu8RYcarREfdjNN+8LitmEHATsLYg== + boxen@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64" @@ -3102,6 +3138,13 @@ debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: dependencies: ms "^2.1.1" +debug@=3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + debug@^3.2.6: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" @@ -4016,6 +4059,13 @@ flush-write-stream@^1.0.0: inherits "^2.0.3" readable-stream "^2.3.6" +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== + dependencies: + debug "=3.1.0" + for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -4424,6 +4474,11 @@ he@1.1.1: resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= +hex-rgb@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/hex-rgb/-/hex-rgb-4.1.0.tgz#2d5d3a2943bd40e7dc9b0d5b98903d7d17035967" + integrity sha512-n7xsIfyBkFChITGPh6FLtxNzAt2HxZLcQIY9hYH4gm2gmMQJHMguMH3E+jnmvUbSTF5QrmFnGab5Ippi+D7e/g== + highlight.js@^9.6.0: version "9.18.3" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.3.tgz#a1a0a2028d5e3149e2380f8a865ee8516703d634" @@ -5508,6 +5563,11 @@ mini-css-extract-plugin@0.8.0: schema-utils "^1.0.0" webpack-sources "^1.1.0" +mini-svg-data-uri@^1.0.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.2.3.tgz#e16baa92ad55ddaa1c2c135759129f41910bc39f" + integrity sha512-zd6KCAyXgmq6FV1mR10oKXYtvmA9vRoB6xPSTUJTbFApCtkefDnYueVR1gkof3KcdLZo1Y8mjF2DFmQMIxsHNQ== + minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" @@ -8381,7 +8441,7 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" -traverse@0.6.6: +traverse@0.6.6, traverse@^0.6.6: version "0.6.6" resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" integrity sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=