diff --git a/package.json b/package.json
index 3c0356a1e..2e79a0d68 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 9a5bf7ba3..000000000
--- 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 000000000..f4aa3d9af
--- /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 000000000..c710e0ddb
--- /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 000000000..54ecab360
--- /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 000000000..2f47239c7
--- /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 a684a3d71..4a591b4b0 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 35d51eac9..4fa631c26 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 5877af118..0a5c08b64 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=