Skip to content

Next.js Integration ​

Fire Shield provides seamless integration with Next.js for both App Router (Next.js 13+) and Pages Router (Next.js 12).

Installation ​

bash
npm install @fire-shield/[email protected] @fire-shield/[email protected]

Setup ​

Initialize RBAC and Adapter ​

typescript
// lib/rbac.ts
import { RBAC } from '@fire-shield/core';
import { NextRBACAdapter } from '@fire-shield/next';
import { getUser } from './auth';

export const rbac = new RBAC();

// Define roles
rbac.createRole('admin', ['user:*', 'post:*']);
rbac.createRole('editor', ['post:read', 'post:write']);
rbac.createRole('viewer', ['post:read']);

// Create the adapter
export const rbacAdapter = new NextRBACAdapter(rbac, {
  getUser: (req) => getUser(req),
});

App Router (Next.js 13+) ​

Middleware Protection ​

Protect routes using Next.js middleware with the adapter's middleware method:

typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { rbacAdapter } from './lib/rbac';

const adminMiddleware = rbacAdapter.middleware('admin:access');

export async function middleware(request: NextRequest) {
  // Protect admin routes
  if (request.nextUrl.pathname.startsWith('/admin')) {
    const response = await adminMiddleware(request);
    if (response) return response; // returns 403 if unauthorized
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/admin/:path*', '/api/admin/:path*'],
};

Route-Based Protection ​

typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { rbac } from './lib/rbac';

const protectedRoutes = {
  '/admin': 'admin:access',
  '/posts/create': 'post:write',
  '/users': 'user:read',
};

export function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname;
  const user = getUserFromRequest(request);

  for (const [route, permission] of Object.entries(protectedRoutes)) {
    if (pathname.startsWith(route)) {
      if (!rbac.hasPermission(user, permission)) {
        return NextResponse.redirect(new URL('/unauthorized', request.url));
      }
    }
  }

  return NextResponse.next();
}

Server Components ​

Check permissions in server components:

typescript
// app/admin/page.tsx
import { rbac } from '@/lib/rbac';
import { getUser } from '@/lib/auth';
import { redirect } from 'next/navigation';

export default async function AdminPage() {
  const user = await getUser();

  if (!rbac.hasPermission(user, 'admin:access')) {
    redirect('/unauthorized');
  }

  return (
    <div>
      <h1>Admin Dashboard</h1>
      {/* Admin content */}
    </div>
  );
}

API Routes ​

Protect API routes using the adapter's withPermission or withRole methods:

typescript
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { rbacAdapter } from '@/lib/rbac';
import { getUsers, createUser } from '@/lib/users';

export const GET = rbacAdapter.withPermission(
  'user:read',
  async (request: NextRequest) => {
    const users = await getUsers();
    return NextResponse.json({ users });
  }
);

export const POST = rbacAdapter.withPermission(
  'user:create',
  async (request: NextRequest) => {
    const body = await request.json();
    const newUser = await createUser(body);
    return NextResponse.json({ user: newUser });
  }
);

You can also check permissions manually:

typescript
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { rbac } from '@/lib/rbac';
import { getUser } from '@/lib/auth';

export async function GET(request: NextRequest) {
  const user = await getUser(request);

  // Check permission with detailed result
  const result = rbac.authorize(user, 'user:read');
  if (!result.allowed) {
    return NextResponse.json(
      { error: 'Forbidden', reason: result.reason },
      { status: 403 }
    );
  }

  const users = await getUsers();
  return NextResponse.json({ users });
}

API Routes with Role Check ​

typescript
// app/api/admin/stats/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { rbacAdapter } from '@/lib/rbac';

export const GET = rbacAdapter.withRole(
  'admin',
  async (request: NextRequest) => {
    const stats = await getAdminStats();
    return NextResponse.json({ stats });
  }
);

Server Actions ​

Use the adapter's requirePermission or requireRole methods in server actions:

typescript
// app/actions/users.ts
'use server';

import { rbacAdapter } from '@/lib/rbac';
import { getUser } from '@/lib/auth';
import { revalidatePath } from 'next/cache';

export async function deleteUser(userId: string) {
  const user = await getUser();

  // Throws if permission is missing
  await rbacAdapter.requirePermission(user, 'user:delete');

  await db.users.delete(userId);
  revalidatePath('/admin/users');

  return { success: true };
}

export async function updateUserRole(userId: string, newRole: string) {
  const user = await getUser();

  await rbacAdapter.requirePermission(user, 'user:update');

  await db.users.update(userId, { role: newRole });
  revalidatePath('/admin/users');

  return { success: true };
}

Client Component Protection ​

Hide/show UI elements based on permissions:

typescript
// components/AdminButton.tsx
'use client';

import { useUser } from '@/hooks/useUser';
import { rbac } from '@/lib/rbac';

export function AdminButton() {
  const user = useUser();

  if (!rbac.hasPermission(user, 'admin:access')) {
    return null;
  }

  return (
    <button onClick={handleAdminAction}>
      Admin Panel
    </button>
  );
}
typescript
// components/PostActions.tsx
'use client';

import { useUser } from '@/hooks/useUser';
import { rbac } from '@/lib/rbac';

export function PostActions({ postId }: { postId: string }) {
  const user = useUser();

  return (
    <div>
      {rbac.hasPermission(user, 'post:read') && (
        <button onClick={() => viewPost(postId)}>View</button>
      )}

      {rbac.hasPermission(user, 'post:write') && (
        <button onClick={() => editPost(postId)}>Edit</button>
      )}

      {rbac.hasPermission(user, 'post:delete') && (
        <button onClick={() => deletePost(postId)}>Delete</button>
      )}
    </div>
  );
}

Pages Router (Next.js 12) ​

API Routes ​

typescript
// pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { rbac } from '@/lib/rbac';
import { getUser } from '@/lib/auth';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const user = await getUser(req);

  if (!rbac.hasPermission(user, 'user:read')) {
    return res.status(403).json({ error: 'Forbidden' });
  }

  const users = await getUsers();
  res.json({ users });
}

Pages Router with Adapter HOC ​

The adapter provides withPermissionPagesRouter and withRolePagesRouter for Pages Router API routes:

typescript
// pages/api/admin/users.ts
import { rbacAdapter } from '@/lib/rbac';

export default rbacAdapter.withPermissionPagesRouter(
  'user:read',
  async (req, res) => {
    const users = await getUsers();
    res.json({ users });
  }
);
typescript
// pages/api/admin/stats.ts
import { rbacAdapter } from '@/lib/rbac';

export default rbacAdapter.withRolePagesRouter(
  'admin',
  async (req, res) => {
    const stats = await getAdminStats();
    res.json({ stats });
  }
);

HOC for Page Protection ​

typescript
// lib/withPermission.ts
import { GetServerSideProps, GetServerSidePropsContext } from 'next';
import { rbac } from './rbac';
import { getUser } from './auth';

export function withPermission(
  permission: string,
  getServerSidePropsFunc?: GetServerSideProps
): GetServerSideProps {
  return async (context: GetServerSidePropsContext) => {
    const user = await getUser(context);

    if (!rbac.hasPermission(user, permission)) {
      return {
        redirect: {
          destination: '/unauthorized',
          permanent: false,
        },
      };
    }

    if (getServerSidePropsFunc) {
      return getServerSidePropsFunc(context);
    }

    return { props: {} };
  };
}

Usage:

typescript
// pages/admin/users.tsx
import { withPermission } from '@/lib/withPermission';

export const getServerSideProps = withPermission('user:read', async () => {
  const users = await getUsers();
  return { props: { users } };
});

export default function UsersPage({ users }) {
  return (
    <div>
      <h1>Users</h1>
      {/* Render users */}
    </div>
  );
}

API Reference ​

new NextRBACAdapter(rbac, options?) ​

Creates a new Next.js adapter instance.

Options:

  • getUser?: (req) => RBACUser | Promise<RBACUser> - Extract user from request
  • onUnauthorized?: (result, req, res?) => Response | void - Custom unauthorized handler
  • onError?: (error, req, res?) => Response | void - Custom error handler

Methods ​

adapter.middleware(permission) ​

Returns an async function (request: NextRequest) => Promise<Response | undefined>. Use in middleware.ts for route-level protection.

adapter.withPermission(permission, handler) ​

HOC for App Router route handlers. Wraps a handler with a permission check.

typescript
export const GET = rbacAdapter.withPermission('user:read', async (req) => {
  return NextResponse.json({ users: await getUsers() });
});

adapter.withRole(role, handler) ​

HOC for App Router route handlers. Wraps a handler with a role check.

typescript
export const GET = rbacAdapter.withRole('admin', async (req) => {
  return NextResponse.json({ stats: await getAdminStats() });
});

adapter.withPermissionPagesRouter(permission, handler) ​

HOC for Pages Router API routes.

adapter.withRolePagesRouter(role, handler) ​

HOC for Pages Router API routes with role check.

adapter.checkPermission(user, permission) ​

Returns Promise<AuthorizationResult>. Use in Server Components or Server Actions.

adapter.requirePermission(user, permission) ​

Throws an error if the user lacks the permission. Use in Server Actions.

adapter.requireRole(user, role) ​

Throws an error if the user lacks the role. Use in Server Actions.

adapter.withNotDenied(permission, handler) ​

HOC that blocks access if the given permission is explicitly denied for the user.

adapter.denyPermission(request, permission) ​

Explicitly deny a permission for the user found in the request.

adapter.allowPermission(request, permission) ​

Remove an explicit deny for the user found in the request.

adapter.isDenied(request, permission) ​

Returns Promise<boolean> indicating whether the permission is explicitly denied.

createPermissionChecker(rbac, getUser) ​

Standalone helper that creates a reusable permission-checking function:

typescript
import { createPermissionChecker } from '@fire-shield/next';

const checkPermission = createPermissionChecker(rbac, getUser);

// In a route or component:
const allowed = await checkPermission(request, 'post:write');

createRoleChecker(getUser) ​

Standalone helper that creates a reusable role-checking function:

typescript
import { createRoleChecker } from '@fire-shield/next';

const checkRole = createRoleChecker(getUser);

const isAdmin = await checkRole(request, 'admin');

Authentication Integration ​

With NextAuth.js ​

typescript
// lib/auth.ts
import { getServerSession } from 'next-auth/next';
import { authOptions } from './authOptions';
import type { RBACUser } from '@fire-shield/core';

export async function getUser(): Promise<RBACUser | null> {
  const session = await getServerSession(authOptions);

  if (!session?.user) {
    return null;
  }

  return {
    id: session.user.id,
    roles: session.user.roles || [],
  };
}
typescript
// middleware.ts
import { getToken } from 'next-auth/jwt';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { rbac } from './lib/rbac';

export async function middleware(request: NextRequest) {
  const token = await getToken({
    req: request,
    secret: process.env.NEXTAUTH_SECRET,
  });

  const user = token
    ? { id: token.sub as string, roles: token.roles as string[] }
    : null;

  if (!user || !rbac.hasPermission(user, 'admin:access')) {
    return NextResponse.redirect(new URL('/unauthorized', request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: '/admin/:path*',
};

Audit Logging ​

Database Audit Logger ​

typescript
// lib/auditLogger.ts
import { BufferedAuditLogger } from '@fire-shield/core';
import { db } from '@/lib/database';

export const auditLogger = new BufferedAuditLogger(
  async (events) => {
    await db.auditLogs.insertMany(
      events.map((event) => ({
        type: event.type,
        userId: event.userId,
        permission: event.permission,
        allowed: event.allowed,
        reason: event.reason,
        context: event.context,
        createdAt: new Date(event.timestamp),
      }))
    );
  },
  {
    maxBufferSize: 100,
    flushIntervalMs: 5000,
  }
);
typescript
// lib/rbac.ts
import { RBAC } from '@fire-shield/core';
import { auditLogger } from './auditLogger';

export const rbac = new RBAC({ auditLogger });

API Route for Audit Logs ​

typescript
// app/api/audit-logs/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { rbacAdapter } from '@/lib/rbac';
import { db } from '@/lib/database';

export const GET = rbacAdapter.withPermission(
  'audit:read',
  async (request: NextRequest) => {
    const logs = await db.auditLogs.find().sort({ createdAt: -1 }).limit(100);
    return NextResponse.json({ logs });
  }
);

Dynamic Permissions ​

Check permissions based on resource ownership:

typescript
// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { rbac } from '@/lib/rbac';
import { getUser } from '@/lib/auth';

export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const user = await getUser(request);
  const post = await getPost(params.id);

  // Check if user owns the post
  const isOwner = post.authorId === user.id;

  // Check permission with ownership
  const canDelete =
    rbac.hasPermission(user, 'post:delete:any') ||
    (isOwner && rbac.hasPermission(user, 'post:delete:own'));

  if (!canDelete) {
    return NextResponse.json(
      { error: 'You do not have permission to delete this post' },
      { status: 403 }
    );
  }

  await deletePost(params.id);
  return NextResponse.json({ success: true });
}

Best Practices ​

1. Centralize Permission Checks ​

typescript
// lib/permissions.ts
import { rbac } from './rbac';
import type { RBACUser } from '@fire-shield/core';

export const permissions = {
  canManageUsers: (user: RBACUser) => rbac.hasPermission(user, 'user:manage'),
  canEditPost: (user: RBACUser, post: Post) =>
    rbac.hasPermission(user, 'post:edit:any') ||
    (post.authorId === user.id && rbac.hasPermission(user, 'post:edit:own')),
  canDeletePost: (user: RBACUser, post: Post) =>
    rbac.hasPermission(user, 'post:delete:any') ||
    (post.authorId === user.id && rbac.hasPermission(user, 'post:delete:own')),
};

2. Use TypeScript for Type Safety ​

typescript
// types/permissions.ts
export type Permission =
  | 'user:read'
  | 'user:write'
  | 'user:delete'
  | 'post:read'
  | 'post:write'
  | 'post:delete'
  | 'admin:access';

export type Role = 'admin' | 'editor' | 'viewer';

3. Create Reusable Middleware ​

typescript
// lib/middleware/requirePermission.ts
import { NextRequest, NextResponse } from 'next/server';
import { rbac } from '../rbac';
import { getUser } from '../auth';

export function requirePermission(permission: string) {
  return async (request: NextRequest) => {
    const user = await getUser(request);

    if (!rbac.hasPermission(user, permission)) {
      return NextResponse.json(
        { error: 'Forbidden', required: permission },
        { status: 403 }
      );
    }

    return NextResponse.next();
  };
}

TypeScript Support ​

typescript
import type { RBACUser } from '@fire-shield/core';

interface User extends RBACUser {
  email: string;
  name: string;
}

const user: User = {
  id: 'user-123',
  roles: ['editor'],
  email: '[email protected]',
  name: 'John Doe',
};

rbac.hasPermission(user, 'post:write');

Next Steps ​