diff --git a/server/overseerr-api.yml b/server/overseerr-api.yml
index 1a36a86d0..461fbd068 100644
--- a/server/overseerr-api.yml
+++ b/server/overseerr-api.yml
@@ -457,10 +457,7 @@ paths:
content:
application/json:
schema:
- type: object
- properties:
- status:
- type: string
+ $ref: '#/components/schemas/User'
requestBody:
required: true
content:
@@ -472,7 +469,23 @@ paths:
type: string
required:
- authToken
-
+ /auth/logout:
+ get:
+ summary: Logout and clear session cookie
+ description: This endpoint will completely clear the session cookie and associated values, logging out the user
+ tags:
+ - auth
+ responses:
+ '200':
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ type: string
+ example: 'ok'
/user:
get:
summary: Returns a list of all users
diff --git a/server/routes/auth.ts b/server/routes/auth.ts
index 8a62528a6..a997a6549 100644
--- a/server/routes/auth.ts
+++ b/server/routes/auth.ts
@@ -75,7 +75,7 @@ authRoutes.post('/login', async (req, res) => {
req.session.userId = user.id;
}
- return res.status(200).json({ status: 'ok' });
+ return res.status(200).json(user?.filter() ?? {});
} catch (e) {
console.error(e);
res
@@ -84,4 +84,17 @@ authRoutes.post('/login', async (req, res) => {
}
});
+authRoutes.get('/logout', (req, res, next) => {
+ req.session?.destroy((err) => {
+ if (err) {
+ return next({
+ status: 500,
+ message: 'Something went wrong while attempting to logout',
+ });
+ }
+
+ return res.status(200).json({ status: 'ok' });
+ });
+});
+
export default authRoutes;
diff --git a/src/components/Layout/UserDropdown/index.tsx b/src/components/Layout/UserDropdown/index.tsx
index 212d2732c..de750f66a 100644
--- a/src/components/Layout/UserDropdown/index.tsx
+++ b/src/components/Layout/UserDropdown/index.tsx
@@ -1,11 +1,20 @@
import React, { useState } from 'react';
import Transition from '../../Transition';
import { useUser } from '../../../hooks/useUser';
+import axios from 'axios';
const UserDropdown: React.FC = () => {
- const { user } = useUser();
+ const { user, revalidate } = useUser();
const [isDropdownOpen, setDropdownOpen] = useState(false);
+ const logout = async () => {
+ const response = await axios.get('/api/v1/auth/logout');
+
+ if (response.data?.status === 'ok') {
+ revalidate();
+ }
+ };
+
return (
@@ -53,6 +62,7 @@ const UserDropdown: React.FC = () => {
href="#"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition ease-in-out duration-150"
role="menuitem"
+ onClick={() => logout()}
>
Sign out
diff --git a/src/components/Login/index.tsx b/src/components/Login/index.tsx
index 372fb9f1a..6c65909ec 100644
--- a/src/components/Login/index.tsx
+++ b/src/components/Login/index.tsx
@@ -16,7 +16,7 @@ const Login: React.FC = () => {
const login = async () => {
const response = await axios.post('/api/v1/auth/login', { authToken });
- if (response.data?.status === 'OK') {
+ if (response.data?.email) {
revalidate();
}
};
diff --git a/src/context/UserContext.tsx b/src/context/UserContext.tsx
index d66fc0d09..93c18ea32 100644
--- a/src/context/UserContext.tsx
+++ b/src/context/UserContext.tsx
@@ -15,7 +15,7 @@ export const UserContext: React.FC
= ({
initialUser,
children,
}) => {
- const { user, revalidate } = useUser({ initialData: initialUser });
+ const { user, error, revalidate } = useUser({ initialData: initialUser });
const router = useRouter();
useEffect(() => {
@@ -23,10 +23,17 @@ export const UserContext: React.FC = ({
}, [router.pathname, revalidate]);
useEffect(() => {
- if (!router.pathname.match(/(setup|login)/) && !user) {
- router.push('/login');
+ let routing = false;
+
+ if (
+ !router.pathname.match(/(setup|login)/) &&
+ (!user || error) &&
+ !routing
+ ) {
+ routing = true;
+ location.href = '/login';
}
- }, [router, user]);
+ }, [router, user, error]);
return <>{children}>;
};
diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts
index 5fa888b3d..754680067 100644
--- a/src/hooks/useUser.ts
+++ b/src/hooks/useUser.ts
@@ -21,7 +21,12 @@ export const useUser = ({
const initialRef = useRef(initialData);
const { data, error, revalidate } = useSwr(
id ? `/api/v1/user/${id}` : `/api/v1/auth/me`,
- { initialData: initialRef.current }
+ {
+ initialData: initialRef.current,
+ refreshInterval: 30000,
+ errorRetryInterval: 30000,
+ shouldRetryOnError: false,
+ }
);
return {
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 28a307e3f..8a368f88b 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -25,22 +25,24 @@ class CoreApp extends App {
);
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();
+ if (ctx.res) {
+ 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 (!router.pathname.match(/(login|setup)/)) {
+ ctx.res.writeHead(307, {
+ Location: '/login',
+ });
+ ctx.res.end();
+ }
}
}