From d7c436522ce28ab1f58c283841e099f96224990c Mon Sep 17 00:00:00 2001 From: Roy Anger Date: Mon, 18 Nov 2024 17:27:19 -0500 Subject: [PATCH] (/guides/basic-rbac): update code examples for Next 15; resolve a few minor issues; update copy (#1669) Co-authored-by: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Co-authored-by: victoria --- docs/guides/basic-rbac.mdx | 80 +++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 44 deletions(-) diff --git a/docs/guides/basic-rbac.mdx b/docs/guides/basic-rbac.mdx index 89f2730d9c..c112e4d34f 100644 --- a/docs/guides/basic-rbac.mdx +++ b/docs/guides/basic-rbac.mdx @@ -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. @@ -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. @@ -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 {} @@ -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**. @@ -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) => { @@ -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() { @@ -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' @@ -144,16 +136,16 @@ 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('/') } @@ -161,15 +153,9 @@ This guide assumes that you're using Next.js App Router, but the concepts can be } ``` - ### 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' @@ -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 } @@ -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 } @@ -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 `` 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 `` 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' @@ -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) }} > - + @@ -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' @@ -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 ( <>