diff --git a/tdrs-frontend/src/components/Header/Header.jsx b/tdrs-frontend/src/components/Header/Header.jsx
index c97646e2f..60158504d 100644
--- a/tdrs-frontend/src/components/Header/Header.jsx
+++ b/tdrs-frontend/src/components/Header/Header.jsx
@@ -10,6 +10,7 @@ import {
} from '../../selectors/auth'
import NavItem from '../NavItem/NavItem'
+import PermissionGuard from '../PermissionGuard'
/**
* This component is rendered on every page and contains the navigation bar.
@@ -28,13 +29,6 @@ function Header() {
const userAccessRequestPending = useSelector(accountIsInReview)
const userAccessRequestApproved = useSelector(accountStatusIsApproved)
- const hasPermission = (permissionName) =>
- user?.roles?.[0]?.permissions?.some(
- (perm) => perm.codename === permissionName
- )
-
- const canViewDataFiles = hasPermission('view_datafile')
-
const menuRef = useRef()
const keyListenersMap = useMemo(() => {
@@ -118,13 +112,16 @@ function Header() {
{authenticated && (
<>
hello, world
+ + + ) + + describe('not allowed', () => { + it('shows not allowed if missing one permission', () => { + setup( + { + auth: { + authenticated: true, + user: { + roles: [{ permissions: [{ codename: 'some_stuff' }] }], + }, + }, + }, + ['allowed'] + ) + + expect(screen.queryByText('hello, world')).not.toBeInTheDocument() + expect(screen.queryByText('not allowed')).toBeInTheDocument() + }) + + it('shows not allowed if missing multiple permissions', () => { + setup( + { + auth: { + authenticated: true, + user: { + roles: [{ permissions: [{ codename: 'allowed' }] }], + }, + }, + }, + ['allowed', 'super_allowed', 'super_duper_allowed'] + ) + + expect(screen.queryByText('hello, world')).not.toBeInTheDocument() + expect(screen.queryByText('not allowed')).toBeInTheDocument() + }) + + it('shows not allowed if multiple roles missing permissions', () => { + setup( + { + auth: { + authenticated: true, + user: { + roles: [ + { permissions: [{ codename: 'allowed' }] }, + { permissions: [{ codename: 'super_allowed' }] }, + { permissions: [{ codename: 'nothing' }] }, + ], + }, + }, + }, + ['allowed', 'super_allowed', 'super_duper_allowed'] + ) + + expect(screen.queryByText('hello, world')).not.toBeInTheDocument() + expect(screen.queryByText('not allowed')).toBeInTheDocument() + }) + + it('shows not allowed if user has no roles', () => { + setup( + { + auth: { + authenticated: true, + user: { + roles: null, + }, + }, + }, + ['anything'] + ) + + expect(screen.queryByText('hello, world')).not.toBeInTheDocument() + expect(screen.queryByText('not allowed')).toBeInTheDocument() + }) + + it('shows not allowed if user has no permissions', () => { + setup( + { + auth: { + authenticated: true, + user: { + roles: [{ permissions: [] }], + }, + }, + }, + ['anything'] + ) + + expect(screen.queryByText('hello, world')).not.toBeInTheDocument() + expect(screen.queryByText('not allowed')).toBeInTheDocument() + }) + + it('shows not allowed if requiresApproval and not approved', () => { + setup( + { + auth: { + authenticated: true, + user: { + roles: [{ permissions: ['anything'] }], + account_approval_status: 'Pending', + }, + }, + }, + [], + true + ) + + expect(screen.queryByText('hello, world')).not.toBeInTheDocument() + expect(screen.queryByText('not allowed')).toBeInTheDocument() + }) + }) + + describe('allowed', () => { + it('shows allowed if no required permissions given', () => { + setup( + { + auth: { + authenticated: true, + user: { + roles: null, + }, + }, + }, + null + ) + + expect(screen.queryByText('hello, world')).toBeInTheDocument() + expect(screen.queryByText('not allowed')).not.toBeInTheDocument() + }) + + it('shows allowed if user matches all permissions', () => { + setup( + { + auth: { + authenticated: true, + user: { + roles: [ + { permissions: [{ codename: 'allowed' }] }, + { permissions: [{ codename: 'super_allowed' }] }, + { permissions: [{ codename: 'super_duper_allowed' }] }, + ], + }, + }, + }, + ['allowed', 'super_allowed', 'super_duper_allowed'] + ) + + expect(screen.queryByText('hello, world')).toBeInTheDocument() + expect(screen.queryByText('not allowed')).not.toBeInTheDocument() + }) + + it('shows allowed if requiresApproval and approved', () => { + setup( + { + auth: { + authenticated: true, + user: { + roles: [{ permissions: ['anything'] }], + account_approval_status: 'Approved', + }, + }, + }, + [], + true + ) + + expect(screen.queryByText('hello, world')).toBeInTheDocument() + expect(screen.queryByText('not allowed')).not.toBeInTheDocument() + }) + }) +}) diff --git a/tdrs-frontend/src/components/PermissionGuard/index.js b/tdrs-frontend/src/components/PermissionGuard/index.js new file mode 100644 index 000000000..9d2b9f3a8 --- /dev/null +++ b/tdrs-frontend/src/components/PermissionGuard/index.js @@ -0,0 +1,3 @@ +import PermissionGuard from './PermissionGuard' + +export default PermissionGuard diff --git a/tdrs-frontend/src/components/PrivateRoute/PrivateRoute.js b/tdrs-frontend/src/components/PrivateRoute/PrivateRoute.js index af1f7ef30..33df4eb64 100644 --- a/tdrs-frontend/src/components/PrivateRoute/PrivateRoute.js +++ b/tdrs-frontend/src/components/PrivateRoute/PrivateRoute.js @@ -1,10 +1,11 @@ import React, { useEffect } from 'react' -import { useNavigate } from 'react-router-dom' +import { Navigate, useNavigate } from 'react-router-dom' import { useSelector, useDispatch } from 'react-redux' import { setAlert, clearAlert } from '../../actions/alert' import { ALERT_INFO } from '../Alert' import PrivateTemplate from '../PrivateTemplate' import IdleTimer from '../IdleTimer/IdleTimer' +import PermissionGuard from '../PermissionGuard' /** * @param {React.ReactNode} children - One or more React components to be @@ -12,9 +13,15 @@ import IdleTimer from '../IdleTimer/IdleTimer' * @param {string} title Page title passed in to PrivateTemplate * which is automatically passed via withRouter */ -function PrivateRoute({ children, title }) { +function PrivateRoute({ + children, + title, + requiredPermissions, + requiresApproval, +}) { const authenticated = useSelector((state) => state.auth.authenticated) const authLoading = useSelector((state) => state.auth.loading) + const navigate = useNavigate() const dispatch = useDispatch() @@ -33,12 +40,22 @@ function PrivateRoute({ children, title }) { } }, [authenticated, authLoading, dispatch, navigate]) - return authenticated ? ( -Hello Private Content