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.