Skip to content

Commit

Permalink
(/guides/basic-rbac): update code examples for Next 15; resolve a few…
Browse files Browse the repository at this point in the history
… minor issues; update copy (#1669)

Co-authored-by: Alexis Aguilar <[email protected]>
Co-authored-by: victoria <[email protected]>
  • Loading branch information
3 people authored Nov 18, 2024
1 parent 1b4cfb8 commit d7c4365
Showing 1 changed file with 36 additions and 44 deletions.
80 changes: 36 additions & 44 deletions docs/guides/basic-rbac.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Implement basic Role Based Access Control (RBAC) with metadata
description: Learn how to leverage Clerk's publicMetadata to implement your own basic Role Based Access Controls.
---

To control which users can access certain parts of your application, you can leverage the [roles feature.](/docs/organizations/roles-permissions#roles) Although Clerk offers a roles feature as part of the feature set for [organizations](/docs/organizations/overview), not every app implements organizations. **This guide covers a workaround to set up a basic Role Based Access Control (RBAC) system for products that don't use Clerk's organizations or roles.**
To control which users can access certain parts of your app, you can use the [roles feature](/docs/organizations/roles-permissions#roles). Although Clerk offers roles as part of the [organizations](/docs/organizations/overview) feature set, not every app implements organizations. **This guide covers a workaround to set up a basic Role Based Access Control (RBAC) system for products that don't use Clerk's organizations or roles.**

This guide assumes that you're using Next.js App Router, but the concepts can be adapted to Next.js Pages Router and Remix.

Expand All @@ -14,8 +14,7 @@ This guide assumes that you're using Next.js App Router, but the concepts can be

To build a basic RBAC system, you first need to make `publicMetadata` available to the application directly from the session token. By attaching `publicMetadata` to the user's session, you can access the data without needing to make a network request each time.

1. Navigate to the [Clerk Dashboard](https://dashboard.clerk.com/last-active?path=sessions).
1. In the top navigation, select **Configure**. Then in the sidebar, select **Sessions**.
1. In the Clerk Dashboard, navigate to the [**Sessions**](https://dashboard.clerk.com/last-active?path=sessions) page.
1. Under the **Customize session token** section, select **Edit**.
1. In the modal that opens, enter the following JSON and select **Save**. If you have already customized your session token, you may need to merge this with what you currently have.

Expand All @@ -31,10 +30,8 @@ This guide assumes that you're using Next.js App Router, but the concepts can be
### Create a global TypeScript definition

1. In your application's root folder, create a `types` directory.
1. Inside this directory, add a `globals.d.ts` file. This file will provide auto-completion and prevent TypeScript errors when working with roles.

For this guide, only the `admin` and `moderator` roles will be defined.
1. In your application's root folder, create a `types/` directory.
1. Inside this directory, create a `globals.d.ts` file with the following code. This file will provide auto-completion and prevent TypeScript errors when working with roles. For this guide, only the `admin` and `moderator` roles will be defined.

```ts {{ filename: 'types/globals.d.ts' }}
export {}
Expand All @@ -55,8 +52,7 @@ This guide assumes that you're using Next.js App Router, but the concepts can be

Later in the guide, you will add a basic admin tool to change a user's role. For now, manually add the `admin` role to your own user account.

1. Navigate to the [Clerk Dashboard](https://dashboard.clerk.com/last-active?path=users) .
1. In the top navigation, select **Users**.
1. In the Clerk Dashboard, navigate to the [**Users**](https://dashboard.clerk.com/last-active?path=users) page.
1. Select your own user account.
1. Scroll down to the **User metadata** section and next to the **Public** option, select **Edit**.
1. Add the following JSON and select **Save**.
Expand All @@ -72,11 +68,10 @@ This guide assumes that you're using Next.js App Router, but the concepts can be
Create a helper function to simplify checking roles.

1. In your application's root directory, create a `utils/` folder.
1. Inside this directory, add a `roles.ts` file.
1. Create a `checkRole()` helper that uses the [`auth()`](/docs/references/nextjs/auth) helper to access the user's session claims. From the session claims, access the `publicMetadata` object to check the user's role. The `checkRole()` helper should accept a role of type `Roles`, which you created in the [Create a global TypeScript definition](#create-a-global-typescript-definition) step. It should return `true` if the user has that role or `false` if they do not.
1. Inside this directory, create a `roles.ts` file with the following code. The `checkRole()` helper uses the [`auth()`](/docs/references/nextjs/auth) helper to access the user's session claims. From the session claims, it accesses the `metadata` object to check the user's role. The `checkRole()` helper accepts a role of type `Roles`, which you created in the [Create a global TypeScript definition](#create-a-global-typescript-definition) step. It returns `true` if the user has that role or `false` if they do not.

```ts {{ filename: 'utils/roles.ts' }}
import { Roles } from '@/types/global'
import { Roles } from '@/types/globals'
import { auth } from '@clerk/nextjs/server'

export const checkRole = async (role: Roles) => {
Expand All @@ -93,8 +88,7 @@ This guide assumes that you're using Next.js App Router, but the concepts can be
Now, it's time to create an admin dashboard. The first step is to create the `/admin` route.

1. In your `app/` directory, create an `admin/` folder.
1. In the `admin/` folder, create a file named `page.tsx`.
1. Add the following placeholder code to the file.
1. In the `admin/` folder, create a `page.tsx` file with the following placeholder code.

```tsx {{ filename: 'app/admin/page.tsx' }}
export default function AdminDashboard() {
Expand All @@ -107,16 +101,14 @@ This guide assumes that you're using Next.js App Router, but the concepts can be
To protect the `/admin` route, choose **one** of the two following methods:

1. **Middleware**: Apply role-based access control globally at the route level. This method restricts access to all routes matching `/admin` before the request reaches the actual page.
1. **Page-Level Role Check**: Apply role-based access control directly in the `/admin` page component. This method protects this specific page. To protect other pages in the admin dashboard, apply this protection to each route.
1. **Page-level role check**: Apply role-based access control directly in the `/admin` page component. This method protects this specific page. To protect other pages in the admin dashboard, apply this protection to each route.

> [!IMPORTANT]
> You only need to follow **one** of the following methods to secure your `/admin` route.
#### Option 1: Protect the `/admin` route using middleware

1. In your app's root directory, create a `middleware.ts` file.
1. Use the `createRouteMatcher()` function to identify routes starting with `/admin`.
1. Apply `clerkMiddleware` to intercept requests to the `/admin` route, and check the user's role in their `publicMetadata` to verify that they have the `admin` role. If they don't, redirect them to the home page.
1. In your app's root directory, create a `middleware.ts` file with the following code. The `createRouteMatcher()` function identifies routes starting with `/admin`. `clerkMiddleware()` intercepts requests to the `/admin` route, and checks the user's role in their `metadata` to verify that they have the `admin` role. If they don't, it redirects them to the home page.

```tsx {{ filename: 'middleware.ts' }}
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
Expand Down Expand Up @@ -144,32 +136,26 @@ This guide assumes that you're using Next.js App Router, but the concepts can be

#### Option 2: Protect the `/admin` route at the page-level

1. Navigate to your `app/admin/page.tsx` file.
1. Use the `checkRole()` function to check if the user has the `admin` role. If they don't, redirect them to the home page.
1. Add the following code to the `app/admin/page.tsx` file. The `checkRole()` function checks if the user has the `admin` role. If they don't, it redirects them to the home page.

```tsx {{ filename: 'app/admin/page.tsx' }}
import { auth } from '@clerk/nextjs/server'
import { checkRole } from '@/utils/roles'
import { redirect } from 'next/navigation'

export default function AdminDashboard() {
export default async function AdminDashboard() {
// Protect the page from users who are not admins
if (!checkRole('admin')) {
const isAdmin = await checkRole('admin')
if (!isAdmin) {
redirect('/')
}

return <p>This is the protected admin dashboard restricted to users with the `admin` role.</p>
}
```

### Add admin tools to find users and manage roles
### Create server actions for managing a user's role

You can use the `checkRole()` function along with server actions to build basic tools for finding users and managing roles.

Create a server action for managing a user's role.

1. In your `app/admin/` directory, create an `_actions.ts` file.
1. Create a server action that sets a user's role. Use the `checkRole()` function to verify that the current user has the `admin` role. If they do, proceed to update the specified user's role using the [JavaScript Backend SDK](/docs/references/backend/user/update-user). This ensures that only administrators can modify user roles.
1. Create a server action that removes a user's role.
1. In your `app/admin/` directory, create an `_actions.ts` file with the following code. The `setRole()` action checks that the current user has the `admin` role before updating the specified user's role using Clerk's [JavaScript Backend SDK](/docs/references/backend/user/update-user). The `removeRole()` action removes the role from the specified user.

```ts {{ filename: 'app/admin/_actions.ts' }}
'use server'
Expand All @@ -178,13 +164,15 @@ This guide assumes that you're using Next.js App Router, but the concepts can be
import { clerkClient } from '@clerk/nextjs/server'

export async function setRole(formData: FormData) {
const client = await clerkClient()

// Check that the user trying to set the role is an admin
if (!checkRole('admin')) {
return { message: 'Not Authorized' }
}

try {
const res = await clerkClient().users.updateUser(formData.get('id') as string, {
const res = await client.users.updateUser(formData.get('id') as string, {
publicMetadata: { role: formData.get('role') },
})
return { message: res.publicMetadata }
Expand All @@ -194,8 +182,10 @@ This guide assumes that you're using Next.js App Router, but the concepts can be
}

export async function removeRole(formData: FormData) {
const client = await clerkClient()

try {
const res = await clerkClient().users.updateUser(formData.get('id') as string, {
const res = await client.users.updateUser(formData.get('id') as string, {
publicMetadata: { role: null },
})
return { message: res.publicMetadata }
Expand All @@ -205,10 +195,9 @@ This guide assumes that you're using Next.js App Router, but the concepts can be
}
```

With the server action set up, now build the `<SearchUsers />` component. This component includes a form for searching users, and when submitted, appends the search term to the URL as a search parameter. Your `/admin` route will then perform a query based on the updated URL.
### Create a component for searching for users

1. In your `app/admin/` directory, create a `SearchUsers.tsx` file.
1. Add the following code to the file.
1. In your `app/admin/` directory, create a `SearchUsers.tsx` file with the following code. The `<SearchUsers />` component includes a form for searching for users. When submitted, it appends the search term to the URL as a search parameter. Your `/admin` route will then perform a query based on the updated URL.

```tsx {{ filename: 'app/admin/SearchUsers.tsx' }}
'use client'
Expand All @@ -230,7 +219,7 @@ This guide assumes that you're using Next.js App Router, but the concepts can be
router.push(pathname + '?search=' + queryTerm)
}}
>
<label htmlFor="search">Search for Users</label>
<label htmlFor="search">Search for users</label>
<input id="search" name="search" type="text" />
<button type="submit">Submit</button>
</form>
Expand All @@ -239,12 +228,11 @@ This guide assumes that you're using Next.js App Router, but the concepts can be
}
```

With the server action and the search form set up, it's time to refactor the `app/admin/page.tsx`. It will check whether a search parameter has been appended to the URL by the search form. If a search parameter is present, it will query for users matching the entered term.
### Refactor the admin dashboard

If one or more users are found, the component will display a list of users, showing their first and last names, primary email address, and current role. Each user will have `Make Admin` and `Make Moderator` buttons, which include hidden inputs for the user ID and role. These buttons will use the `setRole()` server action to update the user's role.
With the server action and the search form set up, it's time to refactor the `app/admin/page.tsx`.

1. Navigate to your `app/admin/page.tsx` file.
1. Replace the code with the following code.
1. Replace the code in your `app/admin/page.tsx` file with the following code. It checks whether a search parameter has been appended to the URL by the search form. If a search parameter is present, it queries for users matching the entered term. If one or more users are found, the component displays a list of users, showing their first and last names, primary email address, and current role. Each user has `Make Admin` and `Make Moderator` buttons, which include hidden inputs for the user ID and role. These buttons use the `setRole()` server action to update the user's role.

```tsx {{ filename: 'app/admin/page.tsx' }}
import { redirect } from 'next/navigation'
Expand All @@ -253,14 +241,18 @@ This guide assumes that you're using Next.js App Router, but the concepts can be
import { clerkClient } from '@clerk/nextjs/server'
import { removeRole, setRole } from './_actions'

export default async function AdminDashboard(params: { searchParams: { search?: string } }) {
export default async function AdminDashboard(params: {
searchParams: Promise<{ search?: string }>
}) {
if (!checkRole('admin')) {
redirect('/')
}

const query = params.searchParams.search
const query = (await params.searchParams).search

const client = await clerkClient()

const users = query ? (await clerkClient().users.getUserList({ query })).data : []
const users = query ? (await client.users.getUserList({ query })).data : []

return (
<>
Expand Down

0 comments on commit d7c4365

Please sign in to comment.