Designing Role-Based Admin Dashboards

September 30, 2024 (1y ago)

Designing Role-Based Admin Dashboards

Admin dashboards are critical for managing complex applications. Building the RCCG Dashboard taught me best practices for scalable, secure role-based systems.

Understanding Role-Based Access Control (RBAC)

Core Concepts

// types/permissions.ts
enum Role {
  ADMIN = "admin",
  MODERATOR = "moderator",
  VIEWER = "viewer",
}
 
interface Permission {
  resource: string; // "posts", "users", "settings"
  action: string; // "create", "read", "update", "delete"
}
 
interface User {
  id: number;
  role: Role;
  permissions: Permission[];
}

Database Schema for RBAC

// schema.prisma
model Role {
  id          Int     @id @default(autoincrement())
  name        String  @unique
  permissions Permission[]
  users       User[]
}
 
model Permission {
  id        Int     @id @default(autoincrement())
  resource  String
  action    String
  roles     Role[]
}
 
model User {
  id    Int     @id @default(autoincrement())
  role  Role    @relation(fields: [roleId], references: [id])
  roleId Int
}

Implementing Access Control

Backend Middleware

// middleware/authorize.ts
import { Request, Response, NextFunction } from "express";
 
export function authorize(requiredPermission: {
  resource: string;
  action: string;
}) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const userId = req.user?.id;
    if (!userId) return res.status(401).json({ error: "Unauthorized" });
 
    const user = await prisma.user.findUnique({
      where: { id: userId },
      include: {
        role: {
          include: {
            permissions: true,
          },
        },
      },
    });
 
    const hasPermission = user?.role.permissions.some(
      (p) =>
        p.resource === requiredPermission.resource &&
        p.action === requiredPermission.action,
    );
 
    if (!hasPermission) {
      return res.status(403).json({ error: "Forbidden" });
    }
 
    next();
  };
}
 
// Usage in routes
router.post(
  "/posts",
  authorize({ resource: "posts", action: "create" }),
  createPost,
);

Frontend Permission Checking

// hooks/usePermission.ts
import { useAuth } from "./useAuth";
 
export function usePermission(resource: string, action: string): boolean {
  const { user } = useAuth();
 
  return user?.permissions?.some(
    (p) => p.resource === resource && p.action === action
  ) ?? false;
}
 
// Usage in components
export function PostsList() {
  const canCreatePost = usePermission("posts", "create");
  const canDeletePost = usePermission("posts", "delete");
 
  return (
    <div>
      {canCreatePost && <button>Create Post</button>}
      {/* Posts list with conditional delete buttons */}
    </div>
  );
}

Building Dashboard Components

Dynamic Navigation Based on Permissions

// components/DashboardNav.tsx
import { useAuth } from "@/hooks/useAuth";
import Link from "next/link";
 
const NAV_ITEMS = [
  {
    label: "Users",
    href: "/admin/users",
    requiredPermission: { resource: "users", action: "read" },
  },
  {
    label: "Posts",
    href: "/admin/posts",
    requiredPermission: { resource: "posts", action: "read" },
  },
  {
    label: "Settings",
    href: "/admin/settings",
    requiredPermission: { resource: "settings", action: "read" },
  },
];
 
export function DashboardNav() {
  const { user } = useAuth();
 
  return (
    <nav>
      {NAV_ITEMS.filter((item) =>
        user?.permissions?.some(
          (p) =>
            p.resource === item.requiredPermission.resource &&
            p.action === item.requiredPermission.action,
        ),
      ).map((item) => (
        <Link key={item.href} href={item.href}>
          {item.label}
        </Link>
      ))}
    </nav>
  );
}

Data Table with Actions

// components/DataTable.tsx
import { Table, Button, Space, Popconfirm } from "antd";
import { usePermission } from "@/hooks/usePermission";
 
interface DataTableProps<T> {
  data: T[];
  columns: any[];
  resource: string;
}
 
export function DataTable<T>({ data, columns, resource }: DataTableProps<T>) {
  const canEdit = usePermission(resource, "update");
  const canDelete = usePermission(resource, "delete");
 
  const actionColumn = {
    title: "Actions",
    render: (_, record: any) => (
      <Space>
        {canEdit && (
          <Button type="link" onClick={() => editRecord(record.id)}>
            Edit
          </Button>
        )}
        {canDelete && (
          <Popconfirm title="Delete?" onConfirm={() => deleteRecord(record.id)}>
            <Button type="link" danger>
              Delete
            </Button>
          </Popconfirm>
        )}
      </Space>
    ),
  };
 
  return <Table columns={[...columns, actionColumn]} dataSource={data} />;
}

Real-Time Features with TanStack Query

// hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
 
export function useUsers() {
  return useQuery({
    queryKey: ["users"],
    queryFn: async () => {
      const response = await fetch("/api/users");
      return response.json();
    },
  });
}
 
export function useUpdateUser() {
  const queryClient = useQueryClient();
 
  return useMutation({
    mutationFn: async (user: User) => {
      const response = await fetch(`/api/users/${user.id}`, {
        method: "PUT",
        body: JSON.stringify(user),
      });
      return response.json();
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["users"] });
    },
  });
}

Dashboard Layouts

Two-Column Layout

// layouts/DashboardLayout.tsx
export function DashboardLayout({ children }: { children: React.ReactNode }) {
  const [collapsed, setCollapsed] = useState(false);
 
  return (
    <Layout>
      <Layout.Sider collapsible collapsed={collapsed} onCollapse={setCollapsed}>
        <DashboardNav />
      </Layout.Sider>
      <Layout>
        <Layout.Header>
          <DashboardHeader />
        </Layout.Header>
        <Layout.Content className="p-4">{children}</Layout.Content>
      </Layout>
    </Layout>
  );
}

Security Considerations

Area Best Practice
Authentication JWT tokens with expiration
Authorization Check permissions on every API call
Data Validation Validate all inputs server-side
Logging Log all admin actions for audit trail
Rate Limiting Prevent brute force attacks

Performance Optimization for Dashboards

// API route with pagination and filtering
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) {
  const { page = 1, limit = 10, search = "" } = req.query;
 
  const users = await prisma.user.findMany({
    skip: (Number(page) - 1) * Number(limit),
    take: Number(limit),
    where: {
      OR: [
        { email: { contains: search as string, mode: "insensitive" } },
        { name: { contains: search as string, mode: "insensitive" } },
      ],
    },
  });
 
  const total = await prisma.user.count();
 
  res.json({
    data: users,
    pagination: {
      total,
      page: Number(page),
      limit: Number(limit),
    },
  });
}

Example: RCCG Dashboard Features

The RCCG Jesus House dashboard includes:

Conclusion

Role-based dashboards are essential for complex applications. By implementing proper RBAC patterns, securing your APIs, and optimizing performance, you create tools that your team will love to use.