From a9461f760d8112f2ae16183e796f706d3392f8ec Mon Sep 17 00:00:00 2001 From: TheCatLady <52870424+TheCatLady@users.noreply.github.com> Date: Mon, 29 Mar 2021 00:16:03 -0400 Subject: [PATCH] feat(ui): Add user requests page (#936) * feat(ui): add user requests page * fix: return error if user attempts to fetch another user's requests without adequate perms * fix(ui): make user name on request page link back to user profile * feat(ui): link user request count to their filtered request list view * fix(frontend): only display user requests on profiles if current user has adequate perms * fix: use 'all' filter for user-filtered request list * fix(frontend): pass userId to router.push() * fix: do not pass userId in query for non-user-filtered requests page * fix(frontend): also allow REQUEST_VIEW perm through route guard * fix(frontend): only link request count to user request list if current user has required perms --- overseerr-api.yml | 10 +++- server/routes/request.ts | 14 +++++ src/components/RequestList/index.tsx | 81 +++++++++++++++++++++++---- src/components/UserList/index.tsx | 18 +++++- src/components/UserProfile/index.tsx | 67 ++++++++++++++-------- src/hooks/useRouteGuard.ts | 11 ++-- src/hooks/useUser.ts | 5 +- src/pages/users/[userId]/requests.tsx | 14 +++++ 8 files changed, 176 insertions(+), 44 deletions(-) create mode 100644 src/pages/users/[userId]/requests.tsx diff --git a/overseerr-api.yml b/overseerr-api.yml index fe73c21c..0fdfdcea 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -3957,6 +3957,8 @@ paths: summary: Get all requests description: | Returns all requests if the user has the `ADMIN` or `MANAGE_REQUESTS` permissions. Otherwise, only the logged-in user's requests are returned. + + If the `requestedBy` parameter is specified, only requests from that particular user ID will be returned. tags: - request parameters: @@ -3984,6 +3986,12 @@ paths: type: string enum: [added, modified] default: added + - in: query + name: requestedBy + schema: + type: number + nullable: true + example: 1 responses: '200': description: Requests returned @@ -4593,7 +4601,7 @@ paths: type: number /media: get: - summary: Return media + summary: Get media description: Returns all media (can be filtered and limited) in a JSON object. tags: - media diff --git a/server/routes/request.ts b/server/routes/request.ts index ead984f6..b7598f4e 100644 --- a/server/routes/request.ts +++ b/server/routes/request.ts @@ -17,6 +17,9 @@ requestRoutes.get('/', async (req, res, next) => { try { const pageSize = req.query.take ? Number(req.query.take) : 10; const skip = req.query.skip ? Number(req.query.skip) : 0; + const requestedBy = req.query.requestedBy + ? Number(req.query.requestedBy) + : null; let statusFilter: MediaRequestStatus[]; @@ -100,9 +103,20 @@ requestRoutes.get('/', async (req, res, next) => { { type: 'or' } ) ) { + if (requestedBy && requestedBy !== req.user?.id) { + return next({ + status: 403, + message: "You do not have permission to view this user's requests.", + }); + } + query = query.andWhere('requestedBy.id = :id', { id: req.user?.id, }); + } else if (requestedBy) { + query = query.andWhere('requestedBy.id = :id', { + id: requestedBy, + }); } const [requests, requestCount] = await query diff --git a/src/components/RequestList/index.tsx b/src/components/RequestList/index.tsx index f5dd0ea5..7af6807f 100644 --- a/src/components/RequestList/index.tsx +++ b/src/components/RequestList/index.tsx @@ -1,8 +1,10 @@ +import Link from 'next/link'; import { useRouter } from 'next/router'; import React, { useEffect, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces'; +import { useUser } from '../../hooks/useUser'; import globalMessages from '../../i18n/globalMessages'; import Button from '../Common/Button'; import Header from '../Common/Header'; @@ -31,6 +33,9 @@ type Sort = 'added' | 'modified'; const RequestList: React.FC = () => { const router = useRouter(); const intl = useIntl(); + const { user } = useUser({ + id: Number(router.query.userId), + }); const [currentFilter, setCurrentFilter] = useState(Filter.PENDING); const [currentSort, setCurrentSort] = useState('added'); const [currentPageSize, setCurrentPageSize] = useState(10); @@ -41,7 +46,9 @@ const RequestList: React.FC = () => { const { data, error, revalidate } = useSWR( `/api/v1/request?take=${currentPageSize}&skip=${ pageIndex * currentPageSize - }&filter=${currentFilter}&sort=${currentSort}` + }&filter=${currentFilter}&sort=${currentSort}${ + router.query.userId ? `&requestedBy=${router.query.userId}` : '' + }` ); // Restore last set filter values on component mount @@ -87,9 +94,26 @@ const RequestList: React.FC = () => { return ( <> - +
-
{intl.formatMessage(messages.requests)}
+
+ {user?.displayName} + + ) : ( + '' + ) + } + > + {intl.formatMessage(messages.requests)} +
@@ -111,7 +135,12 @@ const RequestList: React.FC = () => { name="filter" onChange={(e) => { setCurrentFilter(e.target.value as Filter); - router.push(router.pathname); + router.push({ + pathname: router.pathname, + query: router.query.userId + ? { userId: router.query.userId } + : {}, + }); }} value={currentFilter} className="rounded-r-only" @@ -152,7 +181,12 @@ const RequestList: React.FC = () => { name="sort" onChange={(e) => { setCurrentSort(e.target.value as Sort); - router.push(router.pathname); + router.push({ + pathname: router.pathname, + query: router.query.userId + ? { userId: router.query.userId } + : {}, + }); }} value={currentSort} className="rounded-r-only" @@ -226,7 +260,12 @@ const RequestList: React.FC = () => { onChange={(e) => { setCurrentPageSize(Number(e.target.value)); router - .push(router.pathname) + .push({ + pathname: router.pathname, + query: router.query.userId + ? { userId: router.query.userId } + : {}, + }) .then(() => window.scrollTo(0, 0)); }} value={currentPageSize} @@ -247,9 +286,18 @@ const RequestList: React.FC = () => { disabled={!hasPrevPage} onClick={() => router - .push(`${router.pathname}?page=${page - 1}`, undefined, { - shallow: true, - }) + .push( + { + pathname: `${router.pathname}?page=${page - 1}`, + query: router.query.userId + ? { userId: router.query.userId } + : {}, + }, + undefined, + { + shallow: true, + } + ) .then(() => window.scrollTo(0, 0)) } > @@ -259,9 +307,18 @@ const RequestList: React.FC = () => { disabled={!hasNextPage} onClick={() => router - .push(`${router.pathname}?page=${page + 1}`, undefined, { - shallow: true, - }) + .push( + { + pathname: `${router.pathname}?page=${page + 1}`, + query: router.query.userId + ? { userId: router.query.userId } + : {}, + }, + undefined, + { + shallow: true, + } + ) .then(() => window.scrollTo(0, 0)) } > diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index f8e6aff6..e056fd24 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -101,7 +101,7 @@ const UserList: React.FC = () => { }); const [showBulkEditModal, setShowBulkEditModal] = useState(false); const [selectedUsers, setSelectedUsers] = useState([]); - const { user: currentUser } = useUser(); + const { user: currentUser, hasPermission: currentHasPermission } = useUser(); useEffect(() => { const filterString = window.localStorage.getItem('ul-filter-settings'); @@ -538,7 +538,7 @@ const UserList: React.FC = () => {
- + {user.displayName} @@ -549,7 +549,19 @@ const UserList: React.FC = () => {
-
{user.requestCount}
+ {user.id === currentUser?.id || + currentHasPermission( + [Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW], + { type: 'or' } + ) ? ( + + + {user.requestCount} + + + ) : ( + user.requestCount + )}
{user.userType === UserType.PLEX ? ( diff --git a/src/components/UserProfile/index.tsx b/src/components/UserProfile/index.tsx index a828193b..6966bd2f 100644 --- a/src/components/UserProfile/index.tsx +++ b/src/components/UserProfile/index.tsx @@ -1,3 +1,4 @@ +import Link from 'next/link'; import { useRouter } from 'next/router'; import React, { useCallback, useEffect, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; @@ -225,29 +226,51 @@ const UserProfile: React.FC = () => {
)} -
-
-
- {intl.formatMessage(messages.recentrequests)} + {(user.id === currentUser?.id || + currentHasPermission( + [Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW], + { type: 'or' } + )) && ( + <> + -
-
-
- ( - - ))} - placeholder={} - emptyMessage={intl.formatMessage(messages.norequests)} - /> -
+ ( + + ))} + placeholder={} + emptyMessage={intl.formatMessage(messages.norequests)} + /> + + )} ); }; diff --git a/src/hooks/useRouteGuard.ts b/src/hooks/useRouteGuard.ts index 772cd64d..fb6e0209 100644 --- a/src/hooks/useRouteGuard.ts +++ b/src/hooks/useRouteGuard.ts @@ -1,16 +1,19 @@ -import { Permission, useUser } from './useUser'; import { useRouter } from 'next/router'; import { useEffect } from 'react'; +import { Permission, PermissionCheckOptions, useUser } from './useUser'; -const useRouteGuard = (permission: Permission | Permission[]): void => { +const useRouteGuard = ( + permission: Permission | Permission[], + options?: PermissionCheckOptions +): void => { const router = useRouter(); const { user, hasPermission } = useUser(); useEffect(() => { - if (user && !hasPermission(permission)) { + if (user && !hasPermission(permission, options)) { router.push('/'); } - }, [user, permission, router, hasPermission]); + }, [user, permission, router, hasPermission, options]); }; export default useRouteGuard; diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts index 6f621715..2e737d55 100644 --- a/src/hooks/useUser.ts +++ b/src/hooks/useUser.ts @@ -1,13 +1,14 @@ import useSwr from 'swr'; +import { MutatorCallback } from 'swr/dist/types'; +import { UserType } from '../../server/constants/user'; import { hasPermission, Permission, PermissionCheckOptions, } from '../../server/lib/permissions'; -import { UserType } from '../../server/constants/user'; -import { MutatorCallback } from 'swr/dist/types'; export { Permission, UserType }; +export type { PermissionCheckOptions }; export interface User { id: number; diff --git a/src/pages/users/[userId]/requests.tsx b/src/pages/users/[userId]/requests.tsx new file mode 100644 index 00000000..105b56b4 --- /dev/null +++ b/src/pages/users/[userId]/requests.tsx @@ -0,0 +1,14 @@ +import { NextPage } from 'next'; +import React from 'react'; +import RequestList from '../../../components/RequestList'; +import useRouteGuard from '../../../hooks/useRouteGuard'; +import { Permission } from '../../../hooks/useUser'; + +const UserRequestsPage: NextPage = () => { + useRouteGuard([Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW], { + type: 'or', + }); + return ; +}; + +export default UserRequestsPage;