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(); + } } }