diff --git a/package.json b/package.json index f16e01968..3f011bd42 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "reflect-metadata": "^0.1.13", "sqlite3": "^5.0.0", "swagger-ui-express": "^4.1.4", + "swr": "^0.3.2", "typeorm": "^0.2.25", "yamljs": "^0.3.0" }, @@ -40,9 +41,9 @@ "@types/node": "^14.6.3", "@types/react": "^16.9.49", "@types/react-transition-group": "^4.4.0", - "@typescript-eslint/eslint-plugin": "^4.0.0", "@types/swagger-ui-express": "^4.1.2", "@types/yamljs": "^0.2.31", + "@typescript-eslint/eslint-plugin": "^4.0.0", "@typescript-eslint/parser": "^3.10.1", "commitizen": "^4.2.1", "cz-conventional-changelog": "^3.3.0", diff --git a/server/index.ts b/server/index.ts index 317bc56ac..2fb1a50e2 100644 --- a/server/index.ts +++ b/server/index.ts @@ -35,6 +35,7 @@ app // Setup sessions const sessionRespository = getRepository(Session); server.use( + '/api', session({ secret: 'verysecret', resave: false, diff --git a/src/components/Login/index.tsx b/src/components/Login/index.tsx index 22a7a886a..372fb9f1a 100644 --- a/src/components/Login/index.tsx +++ b/src/components/Login/index.tsx @@ -1,7 +1,38 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import PlexLoginButton from '../PlexLoginButton'; +import { useUser } from '../../hooks/useUser'; +import axios from 'axios'; +import { useRouter } from 'next/dist/client/router'; const Login: React.FC = () => { + const [authToken, setAuthToken] = useState(undefined); + const { user, revalidate } = useUser(); + const router = useRouter(); + + // Effect that is triggered when the `authToken` comes back from the Plex OAuth + // We take the token and attempt to login. If we get a success message, we will + // ask swr to revalidate the user which _shouid_ come back with a valid user. + useEffect(() => { + const login = async () => { + const response = await axios.post('/api/v1/auth/login', { authToken }); + + if (response.data?.status === 'OK') { + revalidate(); + } + }; + if (authToken) { + login(); + } + }, [authToken, revalidate]); + + // Effect that is triggered whenever `useUser`'s user changes. If we get a new + // valid user, we redirect the user to the home page as the login was successful. + useEffect(() => { + if (user) { + router.push('/'); + } + }, [user, router]); + return (
@@ -13,9 +44,7 @@ const Login: React.FC = () => {
- console.log(`auth token is: ${authToken}`) - } + onAuthToken={(authToken) => setAuthToken(authToken)} />
diff --git a/src/context/UserContext.tsx b/src/context/UserContext.tsx new file mode 100644 index 000000000..72139df2a --- /dev/null +++ b/src/context/UserContext.tsx @@ -0,0 +1,28 @@ +import React, { useEffect } from 'react'; +import { User, useUser } from '../hooks/useUser'; +import { useRouter } from 'next/dist/client/router'; + +interface UserContextProps { + initialUser: User; +} + +/** + * This UserContext serves the purpose of just preparing the useUser hooks + * cache on server side render. It also will handle redirecting the user to + * the login page if their session ever becomes invalid. + */ +export const UserContext: React.FC = ({ + initialUser, + children, +}) => { + const { user } = useUser({ initialData: initialUser }); + const router = useRouter(); + + useEffect(() => { + if (!router.pathname.match(/(setup|login)/) && !user) { + router.push('/login'); + } + }, [router, user]); + + return <>{children}; +}; diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts new file mode 100644 index 000000000..fe0419425 --- /dev/null +++ b/src/hooks/useUser.ts @@ -0,0 +1,31 @@ +import useSwr from 'swr'; +import { useRef } from 'react'; +export interface User { + id: number; + email: string; +} + +interface UserHookResponse { + user?: User; + loading: boolean; + error: string; + revalidate: () => Promise; +} + +export const useUser = ({ + id, + initialData, +}: { id?: number; initialData?: User } = {}): UserHookResponse => { + const initialRef = useRef(initialData); + const { data, error, revalidate } = useSwr( + id ? `/api/v1/user/${id}` : `/api/v1/auth/me`, + { initialData: initialRef.current } + ); + + return { + user: data, + loading: !data && !error, + error, + revalidate, + }; +}; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 1c5e578a9..28a307e3f 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,18 +1,75 @@ import React from 'react'; import '../styles/globals.css'; -import App from 'next/app'; +import App, { AppInitialProps } from 'next/app'; +import { SWRConfig } from 'swr'; import Layout from '../components/Layout'; +import { UserContext } from '../context/UserContext'; +import axios from 'axios'; +import { User } from '../hooks/useUser'; + +// Custom types so we can correctly type our GetInitialProps function +// with our combined user prop +// This is specific to _app.tsx. Other pages will not need to do this! +type NextAppComponentType = typeof App; +type GetInitialPropsFn = NextAppComponentType['getInitialProps']; + +interface AppProps { + user: User; +} + +class CoreApp extends App { + public static getInitialProps: GetInitialPropsFn = async (initialProps) => { + // Run the default getInitialProps for the main nextjs initialProps + const appInitialProps: AppInitialProps = await App.getInitialProps( + initialProps + ); + const { ctx, router } = initialProps; + let user = undefined; + try { + // Attempt to get the user by running a request to the local api + const response = await axios.get( + `http://localhost:${process.env.PORT || 3000}/api/v1/auth/me`, + { headers: ctx.req ? { cookie: ctx.req.headers.cookie } : undefined } + ); + user = response.data; + } catch (e) { + // If there is no user, and ctx.res is set (to check if we are on the server side) + // _AND_ we are not already on the login or setup route, redirect to /login with a 307 + // before anything actually renders + if (ctx.res && !router.pathname.match(/(login|setup)/)) { + ctx.res.writeHead(307, { + Location: '/login', + }); + ctx.res.end(); + } + } + + return { ...appInitialProps, user }; + }; -class CoreApp extends App { public render(): JSX.Element { - const { Component, pageProps, router } = this.props; + const { Component, pageProps, router, user } = this.props; + + let component: React.ReactNode; + if (router.asPath === '/login') { - return ; + component = ; + } else { + component = ( + + + + ); } + return ( - - - + axios.get(url).then((res) => res.data), + }} + > + {component} + ); } } diff --git a/yarn.lock b/yarn.lock index 7308d662d..5415a73d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4148,6 +4148,11 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= +fast-deep-equal@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" + integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= + fast-deep-equal@^3.1.1: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -8591,6 +8596,13 @@ swagger-ui-express@^4.1.4: dependencies: swagger-ui-dist "^3.18.1" +swr@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/swr/-/swr-0.3.2.tgz#1197e06553f71afbc42dba758459f12daea96805" + integrity sha512-Bs5Bihq1hQ66O5bdKaL47iZ2nlAaBsd8tTLRLkw9stZeuBEfH7zSuQI95S2TpchL0ybsMq3isWwuso2uPvCfHA== + dependencies: + fast-deep-equal "2.0.1" + table@^5.2.3: version "5.4.6" resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"