Protecting routes
Objectives
Section titled โObjectivesโNow that we have a fully functioning admin review page (see our previous guide), we need to make sure that unauthorised users canโt access it.
Because this page allows users to publish or unpublish articles, we need to ensure that only users with the โadminโ role can access it.
In this guide, we will learn how to protect routes in Remix by using the loader
function to check if a user has the โadminโ role before they can access it.
We will do this across two stages:
- Forbid users from accessing the admin review page who are not logged in or do not have the โadminโ role.
- Provide a link in the navbar to the admin review page only for users with the โadminโ role.
1. Protect the admin review page
Section titled โ1. Protect the admin review pageโThis first step is surprisingly easy because the Epic Stack comes with functions that handle authentication and authorisation!
We therefore only need to add a few lines of code ๐.
Open app/routes/admin-review.tsx
and make the additions shown below:
import { type LoaderFunctionArgs, json } from '@remix-run/node'import { NavLink, Outlet, useLoaderData } from '@remix-run/react'import { prisma } from '~/utils/db.server.ts'import { cn } from '~/utils/misc.tsx'import { requireUserWithRole } from '~/utils/permissions.server.js'
export async function loader({ request }: LoaderFunctionArgs) { await requireUserWithRole(request, 'admin')
const allArticles = await prisma.article.findMany({ select: { id: true, title: true, isPublished: true }, })
return json({ allArticles })}
Test protection with unauthorized user
Section titled โTest protection with unauthorized userโThe way to test this is to log in with the user role you set up for yourself in a previous tutorial.
Once you are logged in, try to access the 'http://localhost:3000/admin-review'
page. You should see a 403 error:
This is fine, but not the best user experience. At this point, the application has completely crashed, and the only way a user can recover is to use the browserโs back button.
Letโs improve this by adding an ErrorBoundary
component to catch the error and display a more user-friendly message.
Add an ErrorBoundary
component
Section titled โAdd an ErrorBoundary componentโLetโs add an ErrorBoundary
component to app/routes/admin-review.tsx
.
First, import the GeneralErrorBoundary
and Button
components:
import { type LoaderFunctionArgs, json } from '@remix-run/node'import { NavLink, Outlet, useLoaderData } from '@remix-run/react'import { prisma } from '~/utils/db.server.ts'import { cn } from '~/utils/misc.tsx'import { requireUserWithRole } from '~/utils/permissions.server.js'import { Button } from '~/components/atoms/Button.js'import { GeneralErrorBoundary } from '~/components/ErrorBoundary.js'
Next, scroll all the way to the bottom of the component file, and add the code below:
import { type LoaderFunctionArgs, json } from '@remix-run/node'import { NavLink, Outlet, useLoaderData } from '@remix-run/react'import { prisma } from '~/utils/db.server.ts'import { cn } from '~/utils/misc.tsx'import { requireUserWithRole } from '~/utils/permissions.server.js'import { Button } from '~/components/atoms/Button.js'import { GeneralErrorBoundary } from '~/components/ErrorBoundary.js'
export async function loader({ request }: LoaderFunctionArgs) {7 collapsed lines
await requireUserWithRole(request, 'admin')
const allArticles = await prisma.article.findMany({ select: { id: true, title: true, isPublished: true }, })
return json({ allArticles })}
12 collapsed lines
interface StatusPillProps { isPublished: boolean}export function StatusPill({ isPublished }: StatusPillProps) { return ( <div className={`rounded-full px-2 py-1 text-xs font-semibold ${isPublished ? 'bg-green-700 text-white' : 'bg-red-700 text-white'}`} > {isPublished ? 'P' : 'D'} </div> )}
export default function ArticlesRoute() {40 collapsed lines
const { allArticles } = useLoaderData<typeof loader>()
const navLinkDefaultClassName = 'line-clamp-2 block rounded-l-full py-2 pl-8 pr-6 text-base lg:text-xl' return ( <main className="container flex h-full min-h-[750px] px-0 py-12 md:px-8"> <div className="grid w-full grid-cols-4 bg-muted pl-2 md:container md:rounded-3xl md:pr-0"> <div className="relative col-span-1"> <div className="absolute inset-0 flex flex-col"> <ul className="overflow-y-auto overflow-x-hidden py-12"> {allArticles.map(article => ( <li key={article.id} className="flex items-center gap-2 p-1 pr-0" > <StatusPill isPublished={article.isPublished} /> <NavLink to={article.id} preventScrollReset prefetch="intent" className={({ isActive }) => cn( navLinkDefaultClassName, isActive && 'w-full bg-accent', ) } > {article.title} </NavLink> </li> ))} </ul> </div> </div> <div className="relative col-span-3 bg-accent md:rounded-r-3xl"> <Outlet /> </div> </div> </main> )}
export function ErrorBoundary() { return ( <GeneralErrorBoundary statusHandlers={{ 403: () => ( <div> <p>You are not allowed to access this page.</p> <p> Please login with an administrator account, or contact support. </p> <Button> <NavLink to="/login">Login</NavLink> </Button> </div> ), }} /> )}
Save your changes and head back to the browser. Try to access the 'http://localhost:3000/admin-review'
page again while logged in with the same user.
You should now see a more helpful message:
Summary
Section titled โSummaryโIn this step, we have:
- Protected the admin review page by adding checking if the user has the โadminโ role.
- Added an
ErrorBoundary
component to catch the 403 error and display a more user-friendly message.