skills/development-skills/role-based-access-control/SKILL.md
Implement role-based access control (RBAC) in web applications using tRPC and React. Use when adding admin/manager-only features, restricting endpoints by user role, or implementing permission systems in full-stack TypeScript applications.
npx skillsauth add abcnuts/manus-skills role-based-access-controlInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
4 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Implement secure role-based access control in full-stack TypeScript applications using tRPC procedures and React components.
protectedProcedure already implementedrole field in database schemauseAuth())Verify the user table includes a role field:
// drizzle/schema.ts or similar
export const user = sqliteTable('user', {
id: integer('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
role: text('role', { enum: ['admin', 'user'] }).notNull().default('user'),
// ... other fields
});
If the role field doesn't exist, add it and run migrations.
Add custom procedures for each role level in your tRPC router file:
// server/routers.ts or similar
import { protectedProcedure, router } from "./_core/trpc";
import { TRPCError } from "@trpc/server";
// Admin-only procedure
const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
if (ctx.user.role !== 'admin') {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Only administrators can access this resource'
});
}
return next({ ctx });
});
// Manager or admin procedure (if you have multiple admin-like roles)
const managerProcedure = protectedProcedure.use(({ ctx, next }) => {
if (ctx.user.role !== 'admin' && ctx.user.role !== 'manager') {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Only managers and administrators can access this resource'
});
}
return next({ ctx });
});
Key points:
protectedProcedure (assumes user is already authenticated)FORBIDDEN error code (403) for permission denialsReplace protectedProcedure with role-specific procedures for restricted endpoints:
export const myRouter = router({
// Public endpoint - anyone can access
getPublicData: publicProcedure.query(async () => {
return await getPublicData();
}),
// Protected endpoint - any authenticated user
getUserData: protectedProcedure.query(async ({ ctx }) => {
return await getUserData(ctx.user.id);
}),
// Admin-only endpoint
getAllUsers: adminProcedure.query(async () => {
return await getAllUsers();
}),
// Manager or admin endpoint
getTeamStats: managerProcedure.query(async () => {
return await getTeamStats();
}),
});
Protect React components and UI elements based on user role:
// pages/AdminDashboard.tsx
import { useAuth } from "@/_core/hooks/useAuth";
import { AlertCircle } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
export function AdminDashboard() {
const { user } = useAuth();
// Check authentication
if (!user) {
return (
<div className="container mx-auto py-8">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Authentication Required</AlertTitle>
<AlertDescription>
Please log in to access this page.
</AlertDescription>
</Alert>
</div>
);
}
// Check authorization
if (user.role !== 'admin') {
return (
<div className="container mx-auto py-8">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Access Denied</AlertTitle>
<AlertDescription>
You do not have permission to access this page. This area is restricted to administrators only.
</AlertDescription>
</Alert>
</div>
);
}
// Render admin content
return (
<div className="container mx-auto py-8">
<h1>Admin Dashboard</h1>
{/* Admin-only content */}
</div>
);
}
Conditionally render navigation links based on user role:
// pages/Home.tsx or Layout.tsx
import { trpc } from "@/lib/trpc";
import { Link } from "wouter";
import { Button } from "@/components/ui/button";
export function Navigation() {
const { data: user } = trpc.auth.me.useQuery();
return (
<nav className="flex items-center gap-4">
<Link href="/dashboard">
<Button variant="ghost">Dashboard</Button>
</Link>
<Link href="/profile">
<Button variant="ghost">Profile</Button>
</Link>
{user?.role === 'admin' && (
<Link href="/admin">
<Button variant="ghost">Admin</Button>
</Link>
)}
{(user?.role === 'admin' || user?.role === 'manager') && (
<Link href="/manager">
<Button variant="ghost">Manager</Button>
</Link>
)}
</nav>
);
}
Key points:
user?.role) to handle loading statesFor applications with more than two roles (e.g., admin, manager, moderator, user):
const roleHierarchy = {
admin: 4,
manager: 3,
moderator: 2,
user: 1,
};
const requireRole = (minRole: keyof typeof roleHierarchy) => {
return protectedProcedure.use(({ ctx, next }) => {
const userRoleLevel = roleHierarchy[ctx.user.role];
const requiredLevel = roleHierarchy[minRole];
if (userRoleLevel < requiredLevel) {
throw new TRPCError({
code: 'FORBIDDEN',
message: `This resource requires ${minRole} role or higher`
});
}
return next({ ctx });
});
};
// Usage
const managerProcedure = requireRole('manager'); // allows manager, admin
const moderatorProcedure = requireRole('moderator'); // allows moderator, manager, admin
For fine-grained permissions (e.g., "user can only edit their own posts"):
const canEditPost = protectedProcedure.use(async ({ ctx, next, input }) => {
const post = await getPostById(input.postId);
if (post.authorId !== ctx.user.id && ctx.user.role !== 'admin') {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'You can only edit your own posts'
});
}
return next({ ctx: { ...ctx, post } });
});
Create a custom hook for consistent role checks across components:
// hooks/useRequireRole.ts
import { useAuth } from "@/_core/hooks/useAuth";
import { useEffect } from "react";
import { useLocation } from "wouter";
export function useRequireRole(requiredRole: 'admin' | 'manager') {
const { user } = useAuth();
const [, setLocation] = useLocation();
useEffect(() => {
if (!user) {
setLocation('/login');
} else if (user.role !== requiredRole && user.role !== 'admin') {
setLocation('/access-denied');
}
}, [user, requiredRole, setLocation]);
return { user, isAuthorized: user?.role === requiredRole || user?.role === 'admin' };
}
// Usage in component
export function AdminDashboard() {
const { user, isAuthorized } = useRequireRole('admin');
if (!isAuthorized) return null; // Will redirect
return <div>Admin content</div>;
}
FORBIDDEN (403) for permission issues, UNAUTHORIZED (401) for authentication issuesError: "Cannot read property 'role' of undefined"
protectedProcedure is properly configuredFrontend shows admin links but backend denies access
User role not updating after database change
tools
Generate comprehensive demonstrations showing how to access projects and work across different environments (Manus terminals, personal computers, team collaboration). Use when users ask "how do I access this from another terminal/computer", "how do I share this with my team", "how do I get this on my Mac", or need clarification on Manus persistence vs GitHub usage.
development
Use when you have a spec or requirements for a multi-step task, before touching code
data-ai
Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always
development
Use when implementing any feature or bugfix, before writing implementation code