Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(/guides/basic-rbac): update code examples for Next 15; resolve a few minor issues; update copy #1669

Merged
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 application, you can leverage Clerk's [roles](/docs/organizations/roles-permissions#roles) feature. 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.**
alexisintech marked this conversation as resolved.
Show resolved Hide resolved

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' }}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example is giving a type error! I think the actions need to return void or Promise<void>, but the actions are returning an object with a message property.

Screenshot 2024-11-13 at 17 30 27

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you share your tsconfig and I can take a look into this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just pushed a commit that resolves this on my end. Let me know if you're still seeing errors.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it still gives me those type errors, but if you are saying you don't have the type errors then it should be fine!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you share your repo? The errors should be gone. There is either something in your repo (and maybe still in this code that I didn't update) or there is something that is suppressing the errors in my editor.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is this repo: aa-clerk-next-app
but I made a repo based off of our updated demo repo, and it works with no errors https://github.com/alexisintech/aa-clerk-nextjs-demo2

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so I wouldn't worry about it!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also got the same errors. After I wrapped the form actions in async and await the functions, everything works.

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
Loading