feat(plex/utils): added Plex OAuth class

pull/4/head
sct 4 years ago
parent 5adfdc1b05
commit 72f9624f1d

@ -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",

@ -1,8 +0,0 @@
import React from 'react';
import '../styles/globals.css';
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default MyApp;

@ -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 <Component {...pageProps} />;
}
}
export default CoreApp;

@ -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<boolean>(false);
const [authToken, setAuthToken] = useState<string>('');
const getPlexLogin = async () => {
setLoading(true);
try {
const authToken = await plexOAuth.login();
setAuthToken(authToken);
setLoading(false);
} catch (e) {
console.log(e.message);
setLoading(false);
}
};
return (
<div>
<span className="inline-flex rounded-md shadow-sm">
<button
type="button"
onClick={() => getPlexLogin()}
disabled={loading}
className="inline-flex items-center px-6 py-3 border border-transparent text-base leading-6 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:shadow-outline-indigo active:bg-indigo-700 transition ease-in-out duration-150"
>
{loading ? 'Loading...' : 'Plex Login'}
</button>
</span>
<div className="mt-4">Auth Token: {authToken}</div>
</div>
);
};
export default PlexText;

@ -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<PlexPin> {
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<string> {
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<string> {
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, string>): string {
return Object.keys(data)
.map(function (key) {
return [key, data[key]].map(encodeURIComponent).join('=');
})
.join('&');
}
}
export default PlexOAuth;

@ -0,0 +1,3 @@
export type Undefinable<T> = T | undefined;
export type Nullable<T> = T | null;
export type Maybe<T> = T | null | undefined;

@ -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',
}),
],
};

@ -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"]
}

@ -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=

Loading…
Cancel
Save