mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
User Context (#51)
* feat(frontend): user Context / useUser hook Adds a UserContext to wrap the app and load/cache the user when the website renders. Also adds the useUser hook to pull in user data anywhere its needed on the site. This commit also adds redirection to the login page for users who are not signed in * fix(frontend): use process.env.PORT for user request on server side (defaults to 3000) * docs(frontend): added documentation/notes for how the user context/login works
This commit is contained in:
@@ -27,6 +27,7 @@
|
|||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"sqlite3": "^5.0.0",
|
"sqlite3": "^5.0.0",
|
||||||
"swagger-ui-express": "^4.1.4",
|
"swagger-ui-express": "^4.1.4",
|
||||||
|
"swr": "^0.3.2",
|
||||||
"typeorm": "^0.2.25",
|
"typeorm": "^0.2.25",
|
||||||
"yamljs": "^0.3.0"
|
"yamljs": "^0.3.0"
|
||||||
},
|
},
|
||||||
@@ -40,9 +41,9 @@
|
|||||||
"@types/node": "^14.6.3",
|
"@types/node": "^14.6.3",
|
||||||
"@types/react": "^16.9.49",
|
"@types/react": "^16.9.49",
|
||||||
"@types/react-transition-group": "^4.4.0",
|
"@types/react-transition-group": "^4.4.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.0.0",
|
|
||||||
"@types/swagger-ui-express": "^4.1.2",
|
"@types/swagger-ui-express": "^4.1.2",
|
||||||
"@types/yamljs": "^0.2.31",
|
"@types/yamljs": "^0.2.31",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.0.0",
|
||||||
"@typescript-eslint/parser": "^3.10.1",
|
"@typescript-eslint/parser": "^3.10.1",
|
||||||
"commitizen": "^4.2.1",
|
"commitizen": "^4.2.1",
|
||||||
"cz-conventional-changelog": "^3.3.0",
|
"cz-conventional-changelog": "^3.3.0",
|
||||||
|
@@ -35,6 +35,7 @@ app
|
|||||||
// Setup sessions
|
// Setup sessions
|
||||||
const sessionRespository = getRepository(Session);
|
const sessionRespository = getRepository(Session);
|
||||||
server.use(
|
server.use(
|
||||||
|
'/api',
|
||||||
session({
|
session({
|
||||||
secret: 'verysecret',
|
secret: 'verysecret',
|
||||||
resave: false,
|
resave: false,
|
||||||
|
@@ -1,7 +1,38 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import PlexLoginButton from '../PlexLoginButton';
|
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 Login: React.FC = () => {
|
||||||
|
const [authToken, setAuthToken] = useState<string | undefined>(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 (
|
return (
|
||||||
<div className="w-full pt-10">
|
<div className="w-full pt-10">
|
||||||
<form className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
<form className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||||
@@ -13,9 +44,7 @@ const Login: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<PlexLoginButton
|
<PlexLoginButton
|
||||||
onAuthToken={(authToken) =>
|
onAuthToken={(authToken) => setAuthToken(authToken)}
|
||||||
console.log(`auth token is: ${authToken}`)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
28
src/context/UserContext.tsx
Normal file
28
src/context/UserContext.tsx
Normal file
@@ -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<UserContextProps> = ({
|
||||||
|
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}</>;
|
||||||
|
};
|
31
src/hooks/useUser.ts
Normal file
31
src/hooks/useUser.ts
Normal file
@@ -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<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUser = ({
|
||||||
|
id,
|
||||||
|
initialData,
|
||||||
|
}: { id?: number; initialData?: User } = {}): UserHookResponse => {
|
||||||
|
const initialRef = useRef(initialData);
|
||||||
|
const { data, error, revalidate } = useSwr<User>(
|
||||||
|
id ? `/api/v1/user/${id}` : `/api/v1/auth/me`,
|
||||||
|
{ initialData: initialRef.current }
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: data,
|
||||||
|
loading: !data && !error,
|
||||||
|
error,
|
||||||
|
revalidate,
|
||||||
|
};
|
||||||
|
};
|
@@ -1,20 +1,77 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import '../styles/globals.css';
|
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 Layout from '../components/Layout';
|
||||||
|
import { UserContext } from '../context/UserContext';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { User } from '../hooks/useUser';
|
||||||
|
|
||||||
class CoreApp extends App {
|
// Custom types so we can correctly type our GetInitialProps function
|
||||||
public render(): JSX.Element {
|
// with our combined user prop
|
||||||
const { Component, pageProps, router } = this.props;
|
// This is specific to _app.tsx. Other pages will not need to do this!
|
||||||
if (router.asPath === '/login') {
|
type NextAppComponentType = typeof App;
|
||||||
return <Component {...pageProps} />;
|
type GetInitialPropsFn = NextAppComponentType['getInitialProps'];
|
||||||
|
|
||||||
|
interface AppProps {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CoreApp extends App<AppProps> {
|
||||||
|
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<User>(
|
||||||
|
`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 (
|
}
|
||||||
|
|
||||||
|
return { ...appInitialProps, user };
|
||||||
|
};
|
||||||
|
|
||||||
|
public render(): JSX.Element {
|
||||||
|
const { Component, pageProps, router, user } = this.props;
|
||||||
|
|
||||||
|
let component: React.ReactNode;
|
||||||
|
|
||||||
|
if (router.asPath === '/login') {
|
||||||
|
component = <Component {...pageProps} />;
|
||||||
|
} else {
|
||||||
|
component = (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SWRConfig
|
||||||
|
value={{
|
||||||
|
fetcher: (url) => axios.get(url).then((res) => res.data),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<UserContext initialUser={user}>{component}</UserContext>
|
||||||
|
</SWRConfig>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CoreApp;
|
export default CoreApp;
|
||||||
|
12
yarn.lock
12
yarn.lock
@@ -4148,6 +4148,11 @@ extsprintf@^1.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
|
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
|
||||||
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
|
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:
|
fast-deep-equal@^3.1.1:
|
||||||
version "3.1.3"
|
version "3.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
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:
|
dependencies:
|
||||||
swagger-ui-dist "^3.18.1"
|
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:
|
table@^5.2.3:
|
||||||
version "5.4.6"
|
version "5.4.6"
|
||||||
resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
|
resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
|
||||||
|
Reference in New Issue
Block a user