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:
- User management with role assignment
- Content management for website pages
- Service scheduling and management
- Member directory
- Analytics and reporting
- Audit logs of all actions
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.