Skip to content

Commit b617b5a

Browse files
maranta1mcrobinson
authored andcommitted
Merge pull request #146 from tupuio/140-protect-pages
feat: add auth property to restrict pages based on role and published…
2 parents a117677 + 099bac7 commit b617b5a

21 files changed

+8991
-4724
lines changed

components/AuthChecker/AuthChecker.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { useSession } from "next-auth/react"
2+
import { ADMIN_ROLE } from "../../constants"
3+
4+
export const ACCESS_DENIED_MESSAGE = 'Access denied'
5+
6+
function AuthChecker({ children, authObject }) {
7+
const { data: session } = useSession()
8+
9+
// if the user does not have a session deny access
10+
if (!session) {
11+
return <div>Access denied</div>
12+
}
13+
14+
// if page requires the user to be published and the user is not published deny access
15+
if (authObject?.publishedOnly && !session?.user?.published) {
16+
return <div>Access denied</div>
17+
}
18+
19+
// if the page requires the user to have a certain role and the user does not have the role deny access
20+
if (authObject?.roles && !authObject.roles.some(item => session?.user?.roles?.includes(item)) && !authObject.roles.includes(ADMIN_ROLE)) {
21+
return <div data-testid>{ACCESS_DENIED_MESSAGE}</div>
22+
}
23+
24+
return children
25+
}
26+
27+
export default AuthChecker
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { render, screen } from "@testing-library/react";
2+
import '@testing-library/jest-dom'
3+
import AuthChecker, { ACCESS_DENIED_MESSAGE } from "./AuthChecker";
4+
import { SessionProvider } from "next-auth/react";
5+
import { MENTEE_ROLE } from "../../constants";
6+
7+
const mockSessionObject = {
8+
"user": {
9+
"name": "test user",
10+
"email": "test@gmail.com",
11+
"published": true,
12+
"roles": ["mentor"]
13+
},
14+
"expires": "2200-08-07T15:44:00.045Z"
15+
}
16+
17+
const AUTHCHECKER_CHILD_TEST_ID = 'authchecker-child'
18+
19+
function renderComponent(authObject = {}, sessionObject = mockSessionObject, ChildComponent) {
20+
return render(
21+
<SessionProvider session={sessionObject}>
22+
<AuthChecker authObject={authObject}>
23+
<div data-testid={AUTHCHECKER_CHILD_TEST_ID}>Test</div>
24+
</AuthChecker>
25+
</SessionProvider>
26+
)
27+
}
28+
29+
describe('AuthChecker', () => {
30+
test('should render children if there is no auth object on the component instance', () => {
31+
renderComponent()
32+
33+
expect(screen.getByTestId(AUTHCHECKER_CHILD_TEST_ID)).toBeInTheDocument()
34+
})
35+
36+
describe('published only components', () => {
37+
test('should render children if the user is published', () => {
38+
const publishedSessionObject = {
39+
...mockSessionObject,
40+
user: {
41+
...mockSessionObject.user,
42+
published: true
43+
}
44+
}
45+
46+
renderComponent({ publishedOnly: true }, publishedSessionObject)
47+
48+
expect(screen.queryByTestId(AUTHCHECKER_CHILD_TEST_ID)).toBeInTheDocument()
49+
})
50+
51+
test('should not render children if the user is not published', () => {
52+
53+
const notPublishedSessionObject = {
54+
...mockSessionObject,
55+
user: {
56+
...mockSessionObject.user,
57+
published: false
58+
}
59+
}
60+
61+
renderComponent({ publishedOnly: true }, notPublishedSessionObject)
62+
63+
expect(screen.queryByTestId(AUTHCHECKER_CHILD_TEST_ID)).not.toBeInTheDocument()
64+
65+
expect(screen.queryByText(ACCESS_DENIED_MESSAGE)).toBeInTheDocument()
66+
})
67+
})
68+
69+
describe('mentee only components', () => {
70+
test('should render children if the user has the mentee role', () => {
71+
const menteeSessionObject = {
72+
...mockSessionObject,
73+
user: {
74+
...mockSessionObject.user,
75+
roles: [MENTEE_ROLE]
76+
}
77+
}
78+
79+
renderComponent({ roles: [MENTEE_ROLE] }, menteeSessionObject)
80+
81+
expect(screen.queryByTestId(AUTHCHECKER_CHILD_TEST_ID)).toBeInTheDocument()
82+
})
83+
84+
test('should not render children if the user does not have the mentee role', () => {
85+
86+
renderComponent({ roles: [MENTEE_ROLE] }, null)
87+
88+
expect(screen.queryByTestId(AUTHCHECKER_CHILD_TEST_ID)).not.toBeInTheDocument()
89+
90+
expect(screen.queryByText(ACCESS_DENIED_MESSAGE)).toBeInTheDocument()
91+
})
92+
})
93+
94+
95+
})

components/Sidebar/MobileNav.js

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,17 @@ import { Box, Flex, HStack, Text, VStack } from "@chakra-ui/layout";
55
import {
66
Menu,
77
MenuButton,
8-
MenuDivider,
98
MenuItem,
109
MenuList,
1110
} from "@chakra-ui/menu";
1211
import { FiChevronDown, FiMenu } from "react-icons/fi";
1312
import { signOut } from "next-auth/react";
1413
import { useRouter } from 'next/router'
1514

16-
const MobileNav = ({ session, onOpen, mode, setMode, ...rest }) => {
15+
const MobileNav = ({ session, onOpen, ...rest }) => {
1716
const name = session?.user?.name;
1817
const router = useRouter();
1918

20-
const toggleMode = () => {
21-
setMode(mode === "mentor" ? "mentee" : "mentor");
22-
router.push('/');
23-
};
24-
2519
return (
2620
<Flex
2721
ml={{ base: 0, md: 60 }}
@@ -69,9 +63,6 @@ const MobileNav = ({ session, onOpen, mode, setMode, ...rest }) => {
6963
ml="2"
7064
>
7165
<Text fontSize="sm">{name}</Text>
72-
<Text fontSize="xs" color="gray.600">
73-
{mode === "mentor" ? "Mentor" : "Mentee"}
74-
</Text>
7566
</VStack>
7667
<Box display={{ base: "none", md: "flex" }}>
7768
<FiChevronDown />
@@ -82,10 +73,6 @@ const MobileNav = ({ session, onOpen, mode, setMode, ...rest }) => {
8273
bg={useColorModeValue("white", "gray.900")}
8374
borderColor={useColorModeValue("gray.200", "gray.700")}
8475
>
85-
<MenuItem onClick={toggleMode}>
86-
Switch to {mode === "mentor" ? "Mentee" : "Mentor"} view
87-
</MenuItem>
88-
<MenuDivider />
8976
<MenuItem onClick={() => signOut()}>Sign out</MenuItem>
9077
</MenuList>
9178
</Menu>

components/Sidebar/SidebarContent.js

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import {
1414
} from "react-icons/fi";
1515
import useSWR from "swr";
1616
import NavItem from "./NavItem";
17+
import { useSession } from "next-auth/react";
18+
import { doesUserHaveRole } from "../../utils/session";
19+
import { MENTOR_ROLE, MENTEE_ROLE } from "../../constants";
1720

1821
const fetcher = (...args) => fetch(...args).then((res) => res.json());
1922

@@ -27,9 +30,13 @@ const SidebarContent = ({ onClose, mode, ...rest }) => {
2730
const requestsCount = requestsData?.count || 0;
2831
const applicationsCount = applicationsData?.count || 0;
2932
const menteesCount = menteesData?.count || 0;
30-
const MentorLinkItems = [
33+
const { data: session } = useSession()
34+
35+
const GeneralLinkItems = [
3136
{ name: "Your profile", icon: FiUser, href: "/profile" },
32-
{ name: "Preferences", icon: FiSliders, href: "/preferences" },
37+
]
38+
39+
const MentorLinkItems = [
3340
{
3441
name: "Requests",
3542
icon: FiInbox,
@@ -40,8 +47,8 @@ const SidebarContent = ({ onClose, mode, ...rest }) => {
4047
];
4148

4249
const MenteeLinkItems = [
43-
{ name: "Your profile", icon: FiUser, href: "/profile" },
4450
{ name: "Find a mentor", icon: FiSearch, href: "/mentors" },
51+
{ name: "Preferences", icon: FiSliders, href: "/preferences" },
4552
{ name: "Applications", icon: FiInbox, href: "/applications", tag: () => applicationsCount, },
4653
{ name: "Mentorships", icon: FiUsers, href: "/mentorships", tag: () => mentorshipsCount, },
4754
];
@@ -56,6 +63,12 @@ const SidebarContent = ({ onClose, mode, ...rest }) => {
5663
links = mode === "mentor" ? MentorLinkItems : MenteeLinkItems;
5764
}
5865

66+
67+
// TODO: handle admins
68+
const shouldRenderMentorLinks = profileData?.published && doesUserHaveRole(session, MENTOR_ROLE)
69+
70+
const shouldRenderMenteeLinks = profileData?.published && doesUserHaveRole(session, MENTEE_ROLE)
71+
5972
return (
6073
<Box
6174
transition="3s ease"
@@ -77,7 +90,75 @@ const SidebarContent = ({ onClose, mode, ...rest }) => {
7790
onClick={onClose}
7891
/>
7992
</Flex>
80-
<Flex h="14" alignItems="center" mx="8" justifyContent="space-between">
93+
<Box mt={50}>
94+
{GeneralLinkItems.map((link) => (
95+
<NavItem
96+
key={link.name}
97+
icon={link.icon}
98+
href={link.href}
99+
bg={mode === "mentor" ? "brand.blue" : "brand.green"}
100+
>
101+
{link.name}
102+
{link.tag && link.tag() > 0 && (
103+
<Tag size="sm" colorScheme="teal" ml={2}>
104+
{link.tag()}
105+
</Tag>
106+
)}
107+
</NavItem>
108+
))}
109+
</Box>
110+
{shouldRenderMentorLinks &&
111+
<>
112+
<Flex h="14" alignItems="center" mx="8" justifyContent="space-between">
113+
<Text textColor="white" fontSize="xl" fontWeight="normal">
114+
Mentor
115+
</Text>
116+
</Flex>
117+
<Box>
118+
{MentorLinkItems.map((link) => (
119+
<NavItem
120+
key={link.name}
121+
icon={link.icon}
122+
href={link.href}
123+
bg={mode === "mentor" ? "brand.blue" : "brand.green"}
124+
>
125+
{link.name}
126+
{link.tag && link.tag() > 0 && (
127+
<Tag size="sm" colorScheme="teal" ml={2}>
128+
{link.tag()}
129+
</Tag>
130+
)}
131+
</NavItem>
132+
))}
133+
</Box>
134+
</>
135+
}
136+
{shouldRenderMenteeLinks &&
137+
<>
138+
<Flex h="14" alignItems="center" mx="8" justifyContent="space-between">
139+
<Text textColor="white" fontSize="xl" fontWeight="normal">
140+
Mentee
141+
</Text>
142+
</Flex>
143+
<Box>
144+
{MenteeLinkItems.map((link) => (
145+
<NavItem
146+
key={link.name}
147+
icon={link.icon}
148+
href={link.href}
149+
bg={mode === "mentor" ? "brand.blue" : "brand.green"}
150+
>
151+
{link.name}
152+
{link.tag && link.tag() > 0 && (
153+
<Tag size="sm" colorScheme="teal" ml={2}>
154+
{link.tag()}
155+
</Tag>
156+
)}
157+
</NavItem>
158+
))}
159+
</Box>
160+
</>}
161+
{/* <Flex h="14" alignItems="center" mx="8" justifyContent="space-between">
81162
<Text textColor="white" fontSize="2xl" fontWeight="normal">
82163
{mode}
83164
</Text>
@@ -104,7 +185,7 @@ const SidebarContent = ({ onClose, mode, ...rest }) => {
104185
tupu.io <ExternalLinkIcon mx="2px" />
105186
</Link>
106187
</NavItem>
107-
</Box>
188+
</Box> */}
108189
</Box>
109190
);
110191
};

components/Sidebar/index.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import SigninPass from "../auth/SigninPass";
77
import MobileNav from "./MobileNav";
88
import SidebarContent from "./SidebarContent";
99

10-
export default function SidebarWithHeader({ mode, setMode, children }) {
10+
export default function SidebarWithHeader({ children }) {
1111
const { data: session } = useSession();
1212
const { isOpen, onOpen, onClose } = useDisclosure();
1313
const router = useRouter();
@@ -22,13 +22,11 @@ export default function SidebarWithHeader({ mode, setMode, children }) {
2222
return <SigninPass />;
2323
}
2424

25-
console.log(session);
2625

2726
return (
2827
<Box minH="100vh" bg="gray.100">
2928
<SidebarContent
3029
onClose={() => onClose}
31-
mode={mode}
3230
display={{ base: "none", md: "block" }}
3331
/>
3432
<Drawer
@@ -41,14 +39,12 @@ export default function SidebarWithHeader({ mode, setMode, children }) {
4139
size="full"
4240
>
4341
<DrawerContent>
44-
<SidebarContent mode={mode} onClose={onClose} />
42+
<SidebarContent onClose={onClose} />
4543
</DrawerContent>
4644
</Drawer>
4745
<MobileNav
4846
session={session}
4947
onOpen={onOpen}
50-
mode={mode}
51-
setMode={setMode}
5248
/>
5349
<Box ml={{ base: 0, md: 60 }} p="4">
5450
{children}

constants/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
export * from './api-constants'
1+
export * from './api-constants'
2+
export * from './roles-constants'

constants/roles-constants.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const ADMIN_ROLE = 'admin'
2+
export const MENTOR_ROLE = 'mentor'
3+
export const MENTEE_ROLE = 'mentee'
4+
export const TEST_ROLE = 'test'

0 commit comments

Comments
 (0)