# Table of contents
# Introduction
Let’s start with implementing Nextjs 14 roles and permissions. This step-by-step guide will help you set up a strong system to manage who can access what in your app.
# Shorter Overview
If you don’t want details this long article, you can check out our short article about this here .
# Prerequisites
Before diving into the implementation of Next.js 14 roles and permissions, ensure you have the following prerequisites in place:
Basic Knowledge of Next.js: Familiarity with Next.js and its core concepts.
Node.js and npm/pnpm Installed: Ensure you have Node.js and npm or pnpm installed on your system.
A PostgreSQL Database: Set up PostgreSQL for managing your application’s data.
Drizzle ORM: Install and configure Drizzle ORM for database interactions.
Auth.js (NextAuth): Set up NextAuth for authentication and session management.
Shadcn UI: Install Shadcn UI for user interface components.
Tailwind CSS: Set up Tailwind CSS for styling your application.
# Database Structure
To manage roles and permissions in Next.js 14, you’ll need the following tables in your database:
Users Table: Each user can have one role.
Roles Table: Defines different roles that can be assigned to users.
Permissions Table: Lists all permissions that can be assigned to roles.
Role_Permissions Table: A pivot table that links roles to permissions.
# Step 1 : Setup Nextjs application
Start by creating a new Next.js project using create-next-app
:
npx create-next-app@latest my-app --typescript --tailwind --eslint
npx shadcn-ui@latest init
Use Shadcn UI installation guide for the latest script.
Install all necessary libraries:
pnpm add dotenv bcryptjs react-toastify zod zsa zsa-react tsx
pnpm add -D @types/bcryptjs
# Step 2 : Setup Drizzle ORM
Follow the guide here to set up Drizzle ORM with node-postgres.
pnpm add drizzle-orm pg
pnpm add -D drizzle-kit @types/pg
Create a folder named server
and place all server-side code there.
server/database/index.ts import dotenv from 'dotenv' ;
dotenv. config ();
import { drizzle } from "drizzle-orm/node-postgres" ;
import { Pool } from "pg" ;
import { pgTable, text, integer, serial } from 'drizzle-orm/pg-core' ;
import { relations } from 'drizzle-orm' ;
const pool = new Pool ({
host: process.env. POSTGRES_HOST ,
port: Number (process.env. POSTGRES_PORT ),
user: process.env. POSTGRES_USER ,
password: process.env. POSTGRES_PASSWORD ,
database: process.env. POSTGRES_DB ,
});
export const rolesTable = pgTable ( 'roles' , {
id: serial ( 'id' ). primaryKey (),
name: text ( 'name' ). notNull (),
});
export const permissionsTable = pgTable ( 'permissions' , {
id: serial ( 'id' ). primaryKey (),
name: text ( 'name' ). notNull (),
});
export const usersTable = pgTable ( 'users' , {
id: serial ( 'id' ). primaryKey (),
name: text ( 'name' ). notNull (),
email: text ( 'email' ). notNull (). unique (),
password: text ( 'password' ). notNull (),
role_id: integer ( 'role_id' ). references (() => rolesTable.id). notNull (),
});
export const rolePermissionsTable = pgTable ( 'role_permissions' , {
role_id: integer ( 'role_id' ). references (() => rolesTable.id). notNull (),
permission_id: integer ( 'permission_id' ). references (() => permissionsTable.id). notNull (),
});
export const userRelations = relations (usersTable, ({ one }) => ({
role: one (rolesTable, {
fields: [usersTable.role_id],
references: [rolesTable.id]
})
}));
export const roleRelations = relations (rolesTable, ({ many }) => ({
users: many (usersTable),
permissions: many (permissionsTable)
}));
export const permissionRelations = relations (permissionsTable, ({ many }) => ({
roles: many (rolePermissionsTable)
}))
export const db = drizzle (pool, {
schema: {
rolesTable,
permissionsTable,
usersTable,
userRelations,
roleRelations,
permissionRelations
}
});
export type IUser = typeof usersTable.$inferSelect;
export type IRole = typeof rolesTable.$inferSelect;
Create a .env
file with the following content:
.env POSTGRES_HOST = "127.0.0.1"
POSTGRES_PORT = 5432
POSTGRES_USER = postgres
POSTGRES_PASSWORD = postgres
POSTGRES_DB = databasename
Add drizzle.config.ts
in root of you application
drizzle.config.ts import { defineConfig } from 'drizzle-kit'
import dotenv from 'dotenv' ;
dotenv.config ();
if (
! process.env.POSTGRES_HOST ||
! process.env.POSTGRES_PORT ||
! process.env.POSTGRES_USER ||
! process.env.POSTGRES_PASSWORD ||
! process.env.POSTGRES_DB
) {
throw Error ( "Please add POSTGRES INFO" );
}
const connectionString = ` postgresql://$ {process.env.POSTGRES_USER} : ${ process . env . POSTGRES_PASSWORD } @$ {process.env.POSTGRES_HOST} : ${ process . env . POSTGRES_PORT } /$ {process.env.POSTGRES_DB}` ;
/** @type { import ( "drizzle-kit" ) .Config } * /
export default defineConfig({
schema: './server/database/index.ts',
out: './drizzle',
dialect: "postgresql",
dbCredentials: {
url: connectionString,
},
verbose: true ,
strict: true ,
})
Update package.json
scripts:
package.json {
"name" : "my-app" ,
"version" : "0.1.0" ,
"private" : true ,
"scripts" : {
"dev" : "next dev" ,
"build" : "next build" ,
"start" : "next start" ,
"lint" : "next lint" ,
"db:generate" : "drizzle-kit generate" ,
"db:migrate" : "drizzle-kit migrate" ,
"db:drop" : "drizzle-kit drop" ,
"db:studio" : "drizzle-kit studio"
},
"dependencies" : {
"class-variance-authority" : "^0.7.0" ,
"clsx" : "^2.1.1" ,
"dotenv" : "^16.4.5" ,
"drizzle-orm" : "^0.32.2" ,
"lucide-react" : "^0.424.0" ,
"next" : "14.2.5" ,
"pg" : "^8.12.0" ,
"react" : "^18" ,
"react-dom" : "^18" ,
"tailwind-merge" : "^2.4.0" ,
"tailwindcss-animate" : "^1.0.7"
},
"devDependencies" : {
"@types/node" : "^20" ,
"@types/pg" : "^8.11.6" ,
"@types/react" : "^18" ,
"@types/react-dom" : "^18" ,
"drizzle-kit" : "^0.23.2" ,
"eslint" : "^8" ,
"eslint-config-next" : "14.2.5" ,
"postcss" : "^8" ,
"tailwindcss" : "^3.4.1" ,
"typescript" : "^5"
}
}
Run the following commands to generate and apply migrations:
pnpm run db:generate
pnpm run db:migrate
# Step 3: Setup Auth.js (NextAuth)
pnpm add next-auth@beta
npx auth secret
Copy the content from .env.local
to .env
and delete .env.local
.
.env POSTGRES_HOST = "127.0.0.1"
POSTGRES_PORT = 5432
POSTGRES_USER = postgres
POSTGRES_PASSWORD = postgres
POSTGRES_DB = test
AUTH_SECRET = "CbAnQXI39PoYI8M+7RVARQTLSlcblW/ck0VNMcQupVU=" # Added by `npx auth`. Read more: https://cli.authjs.dev
Create app/api/auth/[…nextauth]/page.ts
:
app/api/auth/[...nextauth]/page.ts import { handlers } from "@/auth"
export const { GET , POST } = handlers
Create auth.ts
in the root of your project:
auth.ts import NextAuth, { DefaultSession } from "next-auth"
import Credentials from "next-auth/providers/credentials"
import { comparePassword } from "@/utils/password"
import { getUserByEmailAndPassword } from "./server/queries/user"
import { User } from "next-auth" ;
export const { handlers , signIn , signOut , auth } = NextAuth ({
providers: [
Credentials ({
credentials: {
email: {},
password: {},
},
authorize : async ( credentials ) => {
const email = credentials.email as string ;
const password = credentials.password as string ;
const user = await getUserByEmailAndPassword (email);
if ( ! user) {
throw new Error ( "User not found." );
}
const isPasswordValid = await comparePassword ({ plainPassword: password, hashPassword: user.password });
if ( ! isPasswordValid) {
throw new Error ( "Invalid password." );
}
return {
id: user.id. toString (),
name: user.name,
email: user.email,
role_id: user.role_id,
} as User ;
},
}),
],
callbacks: {
async jwt ({ token , user }) {
if (user?.role_id) {
token.role_id = user.role_id
}
return token;
},
session ({ session , token }) {
return {
... session,
user: {
... session.user,
role_id: token.role_id as string ,
},
};
},
},
});
now create utils
folder on root of application and create this 3 files we will need this later:
constants.ts export const permissionList = {
POST_SHOW: 'post-list' ,
POST_CREATE: 'post-create' ,
POST_EDIT: 'post-edit' ,
POST_DELETE: 'post-delete'
}
guard.ts "use server"
import { getMyPermissions } from "@/server/queries/user" ;
import { cache } from "react" ;
export const hasPermission = cache ( async ( permissions : string []) => {
const myPermissions = await getMyPermissions ();
return permissions. every ( permission =>
myPermissions. some ( x => x.permissionName === permission)
);
});
password.ts import bcrypt from 'bcryptjs' ;
export async function makePassword ( plainPassword : string ) : Promise < string > {
try {
const salt = await bcrypt. genSalt ( 10 );
const hash = await bcrypt. hash (plainPassword, salt);
return hash;
} catch (err) {
throw new Error ( 'Error hashing password' );
}
}
export async function comparePassword ({
plainPassword ,
hashPassword
} : {
plainPassword : string ,
hashPassword : string
}) : Promise < boolean > {
try {
return await bcrypt. compare (plainPassword, hashPassword);
} catch (err) {
console. log (err);
throw new Error ( 'Error comparing password' );
}
}
now you will get this error
Property ‘role_id’ does not exist on type ‘User | AdapterUser’.
To fix this create types.d.ts
in root of application:
types.d.ts declare module "next-auth" {
/**
* The shape of the user object returned in the OAuth providers' `profile` callback,
* or the second parameter of the `session` callback, when using a database.
*/
interface User {
id : number
name : string
email : string
password : string
role_id : number
}
interface Session {
user : {
role_id : number ;
} & DefaultSession [ "user" ];
}
}
// The `JWT` interface can be found in the `next-auth/jwt` submodule
import { JWT } from "next-auth/jwt"
declare module "next-auth/jwt" {
/** Returned by the `jwt` callback and `auth`, when using JWT sessions */
interface JWT {
/** OpenID ID Token */
idToken ?: string
}
}
now let’s add queries create dir queries
on server folder and create file
server/queries/user.ts import { eq } from "drizzle-orm"
import { db, permissionsTable, rolePermissionsTable, rolesTable, usersTable } from "../database"
import { auth } from "@/auth" ;
import { cache } from "react" ;
export const getRoles = cache ( async () => {
const result = await db.query.rolesTable. findMany ();
if ( ! result) throw Error ( 'No Roles found.' );
return result;
})
export const getCurrentUser = cache ( async () => {
const session = await auth ();
const result = await db.query.usersTable. findFirst ({
where: eq (usersTable.email, session?.user.email),
with: {
role: true
}
})
if ( ! result) {
throw Error ( 'No Users found.' )
};
console. log ({ result });
return result;
})
export const getUserByEmailAndPassword = async ( email : string ) => {
const result = await db.query.usersTable. findFirst ({
where: eq (usersTable.email, email)
});
if ( ! result) throw Error ( 'No Users found.' );
return result;
}
export const getMyPermissions = cache ( async () => {
const session = await auth ();
if ( ! session?.user?.role_id) {
throw new Error ( "Role id not found in session." );
}
const permissions = await db
. select ({
permissionName: permissionsTable.name
})
. from (rolePermissionsTable)
. innerJoin (permissionsTable, eq (rolePermissionsTable.permission_id, permissionsTable.id))
. where ( eq (rolePermissionsTable.role_id, session.user.role_id));
return permissions;
});
# Step 4 : Setup Ui for Role-Based Access Control (RBAC)
Create a wrapper component to use the client-side library.
components/Wrapper.tsx "use client"
import 'react-toastify/dist/ReactToastify.css' ;
import { ToastContainer } from 'react-toastify' ;
import { PropsWithChildren, ReactPropTypes } from 'react' ;
export default function Wrapper ({
children
} : PropsWithChildren ){
return (
<>
{children}
< ToastContainer />
</>
)
}
Now, let’s create the login and register pages.
app/(auth)/login/page.tsx import Link from 'next/link' ;
import LoginForm from './form' ;
import { auth } from '@/auth' ;
import { redirect } from 'next/navigation' ;
export default async function SignInPage () {
const session = await auth ();
if (session) {
redirect ( '/' )
}
return (
< div className = "flex items-center justify-center min-h-screen bg-gray-100" >
< div className = "w-full max-w-md p-8 bg-white shadow-md rounded-lg" >
< h1 className = "text-2xl font-bold mb-6 text-center" >Login</ h1 >
< LoginForm />
< div className = "mt-4 text-center" >
< p className = "text-gray-600" >
{ "Don't have an account?" } { '' }
< Link href = "/register" className = "text-blue-600 hover:underline" >
register
</ Link >
</ p >
</ div >
</ div >
</ div >
);
}
app/(auth)/login/login.tsx "use client" ;
import { Button } from "@/components/ui/button" ;
import { Input } from "@/components/ui/input" ;
import { loginAction } from "@/server/actions/user.action" ;
import { FormEvent } from "react" ;
import { useServerAction } from "zsa-react" ;
export default function LoginForm () {
const { isPending , execute , error } = useServerAction (loginAction);
const handleLogin = async ( event : FormEvent < HTMLFormElement >) => {
event. preventDefault ()
const form = event.currentTarget
const formData = new FormData (form)
const [ data , error ] = await execute (formData)
console. log (data, error)
}
return (
< form
className = "space-y-4"
onSubmit = {handleLogin}
>
{error?.code === 'ERROR' && (
< div className = "p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg" role = "alert" >
< span className = "font-medium" >Error!</ span > {error.message}
</ div >
)}
< label className = "block" >
< span className = "text-gray-700" >Email</ span >
< Input
name = "email"
type = "email"
placeholder = "Email address"
/>
{error?.formattedErrors?.email && (< p className = "text-red-500" >{error?.formattedErrors?.email?._errors}</ p >)}
</ label >
< label className = "block" >
< span className = "text-gray-700" >Password</ span >
< Input
name = "password"
type = "password"
placeholder = "Email address"
/>
{error?.formattedErrors?.password && (< p className = "text-red-500" >{error?.formattedErrors?.password?._errors}</ p >)}
</ label >
< Button
disabled = {isPending}
type = "submit"
className = 'w-full'
>
{isPending ? 'Sign In...' : 'Sign In' }
</ Button >
</ form >
)
}
Now, let’s add all ShadCN components:
pnpm dlx shadcn-ui@latest add button
pnpm dlx shadcn-ui@latest add input
pnpm dlx shadcn-ui@latest add dropdown-menu
pnpm dlx shadcn-ui@latest add sheet
Next, let’s add all actions:
server/actions/user.action.ts "use server"
import bcrypt from 'bcryptjs' ;
import { createServerAction } from "zsa"
import z from "zod"
import { db, usersTable } from "../database" ;
import { eq } from "drizzle-orm" ;
import { comparePassword, makePassword } from "@/utils/password" ;
import { signIn, signOut } from "@/auth" ;
import { AuthError } from "next-auth" ;
function isRedirectError ( error : Error & { digest ?: string }) {
return !! error.digest?. startsWith ( "NEXT_REDIRECT" )
}
export const loginAction = createServerAction ()
. input (z. object ({
email: z. string (). min ( 3 ). email (),
password: z. string (). min ( 3 )
}), {
type: "formData" ,
})
. handler ( async ({ input }) => {
const user = await db.query.usersTable. findFirst ({
where: ( eq (usersTable.email, input.email))
})
if ( ! user) {
throw new Error ( 'User not found.' );
}
if ( !await bcrypt. compare (input.password, user.password)) {
throw new Error ( 'User not found.' );
}
try {
await signIn ( "credentials" , {
email: input.email,
password: input.password,
redirectTo: "/"
});
} catch ( error : any ) {
if ( isRedirectError (error)) throw error; //https://github.com/nextauthjs/next-auth/discussions/9389
if (error instanceof AuthError ) {
return {
errors: undefined ,
message: error.cause?.err?.message
}
}
}
return {
success: true ,
data: user
};
});
export const registerAction = createServerAction ()
. input (z. object ({
name: z. string (). min ( 3 ),
email: z. string (). min ( 3 ). email (),
password: z. string (). min ( 3 ),
role_id: z. string (). min ( 1 )
}), {
type: "formData" ,
})
. handler ( async ({ input }) => {
let user = await db.query.usersTable. findFirst ({
where: ( eq (usersTable.email, input.email))
})
if (user) {
throw new Error ( 'User Already exists.' );
}
const res = await db. insert (usersTable). values ({
email: input.email,
name: input.name,
password: await makePassword (input.password),
role_id: parseInt (input.role_id)
}). returning ();
if (res[ 0 ]){
user = res[ 0 ];
}
return {
success: true ,
data: user
};
});
export const logout = async () => {
await signOut ();
}
At this point, you should get a login page like this:
Now, let’s build the seeder so we can log in.
In package.json
, add this line:
"db:seed" : "tsx ./server/database/seed/index.ts"
Create the seed file at server/database/seed/index.ts
:
server/database/seed/index.ts import pg from "pg" ;
import dotenv from 'dotenv' ;
import { drizzle } from "drizzle-orm/node-postgres" ;
import * as Schema from "../../database" ;
import { makePassword } from "@/utils/password" ;
import { sql } from "drizzle-orm" ;
dotenv. config ();
export const client = new pg. Client ({
host: process.env. POSTGRES_HOST ,
port: Number (process.env. POSTGRES_PORT ),
user: process.env. POSTGRES_USER ,
password: process.env. POSTGRES_PASSWORD ,
database: process.env. POSTGRES_DB ,
});
async function main () {
try {
await client. connect ();
const db = drizzle (client, {
schema: Schema
});
await db. execute ( sql `TRUNCATE TABLE role_permissions CASCADE` );
await db. execute ( sql `TRUNCATE TABLE users CASCADE` );
await db. execute ( sql `TRUNCATE TABLE roles CASCADE` );
await db. execute ( sql `TRUNCATE TABLE permissions CASCADE` );
await db. execute ( sql `ALTER SEQUENCE roles_id_seq RESTART WITH 1` );
await db. execute ( sql `ALTER SEQUENCE permissions_id_seq RESTART WITH 1` );
await db. execute ( sql `ALTER SEQUENCE users_id_seq RESTART WITH 1` );
// Insert roles
const roles = await db. insert (Schema.rolesTable). values ([
{ name: 'admin' },
{ name: 'editor' },
{ name: 'user' },
]). returning ();
// Insert permissions
const permissions = await db. insert (Schema.permissionsTable). values ([
{ name: 'post-list' },
{ name: 'post-create' },
{ name: 'post-edit' },
{ name: 'post-delete' }
]). returning ();
const getRoleId = ( roleName : string ) => {
const role = roles. find ( role => role.name === roleName);
if ( ! role) {
throw new Error ( `Role ${ roleName } not found` );
}
return role.id;
};
const getPermissionId = ( permissionName : string ) => {
const permission = permissions. find ( permission => permission.name === permissionName);
if ( ! permission) {
throw new Error ( `Permission ${ permissionName } not found` );
}
return permission.id;
};
const rolePermissions = [
// Admin has all permissions
{
role_id: getRoleId ( 'admin' ),
permission_id: getPermissionId ( 'post-list' )
},
{
role_id: getRoleId ( 'admin' ),
permission_id: getPermissionId ( 'post-create' )
},
{
role_id: getRoleId ( 'admin' ),
permission_id: getPermissionId ( 'post-edit' )
},
{
role_id: getRoleId ( 'admin' ),
permission_id: getPermissionId ( 'post-delete' )
},
// Editor has all permissions except delete
{
role_id: getRoleId ( 'editor' ),
permission_id: getPermissionId ( 'post-create' )
},
{
role_id: getRoleId ( 'editor' ),
permission_id: getPermissionId ( 'post-edit' )
},
// User has only view permission
{
role_id: getRoleId ( 'user' ),
permission_id: getPermissionId ( 'post-list' )
}
];
await db. insert (Schema.rolePermissionsTable). values (rolePermissions);
await db. insert (Schema.usersTable). values ([
{
name: 'Admin' ,
email: 'admin@example.com' ,
password: await makePassword ( 'password' ),
role_id: getRoleId ( 'admin' )
},
{
name: 'Editor' ,
email: 'editor@example.com' ,
password: await makePassword ( 'password' ),
role_id: getRoleId ( 'editor' )
},
{
name: 'User' ,
email: 'user@example.com' ,
password: await makePassword ( 'password' ),
role_id: getRoleId ( 'user' )
}
]);
console. log ( "Seeding completed successfully!" );
} catch (error) {
console. error ( "Error during seeding:" , error);
} finally {
await client. end ();
}
}
main ();
pnpm db:seed
Next, let’s update the login functionality and home page. First, delete page.tsx and create a folder called (dashboard) for the protected group route. Add a layout for the look and feel of the admin panel and to protect pages and UI:
app/(dashboard)/layout.tsx import Link from "next/link"
import {
CircleUser,
Coffee,
Home,
Menu,
Users,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
import { auth } from "@/auth"
import { redirect } from "next/navigation"
import Logout from "./logout"
import Guard from "@/components/server/Guard"
import { permissionList } from "@/utils/constants"
const Nav = () =>
< nav className = "grid items-start px-2 text-sm font-medium lg:px-4" >
< Link
href = "/"
className = "flex items-center gap-3 rounded-lg px-3 py-2 text-muted-foreground transition-all hover:text-primary"
>
< Home className = "h-4 w-4" />
Dashboard
</ Link >
< Guard
permissions = {[permissionList. POST_SHOW ]}
>
< Link
href = "/users"
className = "flex items-center gap-3 rounded-lg px-3 py-2 text-muted-foreground transition-all hover:text-primary"
>
< Users className = "h-4 w-4" />
Users
</ Link >
</ Guard >
</ nav >
export default async function DashboardLayout (
{ children } :
Readonly <{
children : React . ReactNode ;
}>) {
const session = await auth ();
if ( ! session) {
redirect ( '/login' )
}
return (
< div className = "grid min-h-screen w-full md:grid-cols-[220px_1fr] lg:grid-cols-[280px_1fr]" >
< div className = "hidden border-r bg-muted/40 md:block" >
< div className = "flex h-full max-h-screen flex-col gap-2" >
< div className = "flex h-14 items-center border-b px-4 lg:h-[60px] lg:px-6" >
< Link href = "/" className = "flex items-center gap-2 font-semibold" >
< Coffee className = "h-6 w-6" />
< span className = "" >Coding Tricks</ span >
</ Link >
</ div >
< div className = "flex-1" >
< Nav />
</ div >
</ div >
</ div >
< div className = "flex flex-col" >
< header className = "flex h-14 items-center gap-4 border-b bg-muted/40 px-4 lg:h-[60px] lg:px-6" >
< Sheet >
< SheetTrigger asChild >
< Button
variant = "outline"
size = "icon"
className = "shrink-0 md:hidden"
>
< Menu className = "h-5 w-5" />
< span className = "sr-only" >Toggle navigation menu</ span >
</ Button >
</ SheetTrigger >
< SheetContent side = "left" className = "flex flex-col" >
< div className = "mt-10" >
< Nav />
</ div >
</ SheetContent >
</ Sheet >
< div className = "w-full flex-1" >
</ div >
< DropdownMenu >
< DropdownMenuTrigger asChild >
< Button variant = "secondary" size = "icon" className = "rounded-full" >
< CircleUser className = "h-5 w-5" />
< span className = "sr-only" >Toggle user menu</ span >
</ Button >
</ DropdownMenuTrigger >
< DropdownMenuContent align = "end" >
< DropdownMenuLabel >{session.user?.email}</ DropdownMenuLabel >
< DropdownMenuSeparator />
< Logout />
</ DropdownMenuContent >
</ DropdownMenu >
</ header >
{children}
</ div >
</ div >
)
}
Create the logout component since it needs to be a client component:
app/(dashboard)/logout.tsx "use client"
import { DropdownMenuItem } from "@/components/ui/dropdown-menu"
import { logout } from "@/server/actions/user.action"
export default function Logout () {
return < DropdownMenuItem
onClick = {() => logout ()}
>Logout</ DropdownMenuItem >
}
Create a Guard component to check permissions and show/hide
UI elements accordingly:
components/server/Guard.tsx "use server" ;
import { getMyPermissions } from "@/server/queries/user" ;
import { ReactNode } from "react" ;
interface GuardProps {
permissions : string [];
children : ReactNode ;
}
export default async function Guard ({ permissions , children } : GuardProps ) {
const myPermissions = await getMyPermissions ();
const hasPermissions = permissions. every ( permission =>
myPermissions. some ( x => x.permissionName === permission)
);
if ( ! hasPermissions) {
return null ;
}
return (
<>
{children}
</>
);
}
Add a users page that connects to the users tab from the sidebar:
app/(dashboard)/users/page.tsx import { permissionList } from '@/utils/constants'
import { hasPermission } from '@/utils/guard'
import { redirect } from 'next/navigation' ;
import React from 'react'
export default async function UserPage () {
const permission = await hasPermission ([permissionList. POST_SHOW ]);
if ( ! permission) {
return redirect ( '/' )
}
return (
< div >You have User page access</ div >
)
}
When logged in as admin, you should see all permissions. This button UI is protected by the Guard.tsx
component.
Now log in with editor credentials:
email : editor@example.com
passwod : password
You will see that the show
and delete
permissions are not displayed, and the sidebar users tab
is also removed. Check if the /users
page is accessible. Since the editor doesn’t have the post-show
permission, it should redirect to the home page:
const permission = await hasPermission ([permissionList. POST_SHOW ]);
if ( ! permission) {
return redirect ( '/' )
}
You can add as many permissions as you want to allow.
# Conclusion
You’ve successfully implemented a role-based access control (RBAC) system in your Next.js 14 application using Drizzle ORM and NextAuth. This setup allows you to manage user roles and permissions effectively, ensuring that only authorized users can access specific parts of your application.
You can refer to this GitHub repository for the full code: Next.js Role and Permission .