Hireable LogoHireable
Frontend

RBAC (Role-Based Access Control)

Permission system for controlling access to features and UI elements

Overview

The RBAC system controls access to features based on user roles. It's implemented in @/lib/auth and provides both programmatic checks and React components for UI gating.

Roles

Four roles are defined in the system:

type Role = "ADMIN" | "EMPLOYER" | "TALENT" | "GUEST";
RoleDescription
ADMINFull system access, can manage all resources
EMPLOYERCan post jobs, manage trials, review applications
TALENTCan apply to jobs, participate in trials
GUESTRead-only access to public job listings

Permissions

Permissions are granular access rights:

type Permission =
  // User permissions
  | "users:read"
  | "users:write"
  | "users:delete"
  // Profile permissions
  | "profile:read"
  | "profile:write"
  // Job permissions
  | "jobs:read"
  | "jobs:write"
  | "jobs:delete"
  | "jobs:apply"
  // Application permissions
  | "applications:read"
  | "applications:write"
  | "applications:review"
  // Trial permissions
  | "trials:read"
  | "trials:write"
  | "trials:manage"
  // Settings permissions
  | "settings:read"
  | "settings:write";

Role-Permission Mapping

const ROLE_PERMISSIONS: Record<Role, Permission[]> = {
  ADMIN: [
    "users:read", "users:write", "users:delete",
    "profile:read", "profile:write",
    "jobs:read", "jobs:write", "jobs:delete",
    "applications:read", "applications:write", "applications:review",
    "trials:read", "trials:write", "trials:manage",
    "settings:read", "settings:write",
  ],
  EMPLOYER: [
    "profile:read", "profile:write",
    "jobs:read", "jobs:write", "jobs:delete",
    "applications:read", "applications:review",
    "trials:read", "trials:write", "trials:manage",
  ],
  TALENT: [
    "profile:read", "profile:write",
    "jobs:read", "jobs:apply",
    "applications:read",
    "trials:read",
  ],
  GUEST: [
    "jobs:read",
  ],
};

Permission Functions

Check Single Permission

import { hasPermission } from "@/lib/auth";
 
if (hasPermission("EMPLOYER", "jobs:write")) {
  // User can create/edit jobs
}

Check All Permissions

import { hasAllPermissions } from "@/lib/auth";
 
if (hasAllPermissions("ADMIN", ["users:read", "users:write"])) {
  // User has both permissions
}

Check Any Permission

import { hasAnyPermission } from "@/lib/auth";
 
if (hasAnyPermission("TALENT", ["jobs:apply", "jobs:write"])) {
  // User has at least one permission
}

Get All Permissions

import { getPermissions } from "@/lib/auth";
 
const permissions = getPermissions("EMPLOYER");
// ["profile:read", "profile:write", "jobs:read", ...]

React Guard Components

HasPermission

Renders children only if user has the specified permission:

import { HasPermission } from "@/lib/auth";
 
function JobActions() {
  return (
    <div>
      <HasPermission permission="jobs:write">
        <Button>Create Job</Button>
      </HasPermission>
 
      <HasPermission permission="jobs:delete" fallback={<span>View Only</span>}>
        <Button variant="destructive">Delete Job</Button>
      </HasPermission>
    </div>
  );
}

HasAnyPermission

Renders if user has any of the specified permissions:

import { HasAnyPermission } from "@/lib/auth";
 
function ApplicationActions() {
  return (
    <HasAnyPermission permissions={["applications:write", "applications:review"]}>
      <Button>Review Application</Button>
    </HasAnyPermission>
  );
}

HasRole

Renders if user has one of the specified roles:

import { HasRole } from "@/lib/auth";
 
function AdminPanel() {
  return (
    <HasRole roles={["ADMIN"]} fallback={<AccessDenied />}>
      <AdminDashboard />
    </HasRole>
  );
}
 
function EmployerOrAdmin() {
  return (
    <HasRole roles={["ADMIN", "EMPLOYER"]}>
      <ManageJobs />
    </HasRole>
  );
}

RequireAuth

Renders only if user is authenticated:

import { RequireAuth } from "@/lib/auth";
 
function ProtectedPage() {
  return (
    <RequireAuth fallback={<LoginPrompt />}>
      <Dashboard />
    </RequireAuth>
  );
}

Auth Context Integration

Guards use the AuthContext to get the current user:

import { useAuthContext } from "@/providers/AuthContext";
 
function CustomGuard({ children }) {
  const { user, isAuthenticated, isLoading } = useAuthContext();
 
  if (isLoading) return <Spinner />;
  if (!isAuthenticated) return <LoginPrompt />;
 
  const userRole = user.role.toUpperCase() as Role;
  if (!hasPermission(userRole, "custom:permission")) {
    return <AccessDenied />;
  }
 
  return children;
}

HOC Pattern

For class components or route protection:

import { withAuth } from "@/providers/AuthContext";
 
const ProtectedComponent = withAuth(MyComponent);
 
// Usage
<ProtectedComponent someProp="value" />

Backend Enforcement

While frontend guards provide UI gating, the backend must also enforce permissions:

// Backend middleware example
function requirePermission(permission: Permission) {
  return (req, res, next) => {
    const userRole = req.user?.role?.toUpperCase();
    if (!hasPermission(userRole, permission)) {
      return res.status(403).json({ error: "Forbidden" });
    }
    next();
  };
}
 
// Usage in routes
router.post("/jobs", requirePermission("jobs:write"), createJob);
router.delete("/jobs/:id", requirePermission("jobs:delete"), deleteJob);

Best Practices

  1. Always check on backend - Frontend guards are for UX, not security
  2. Use specific permissions - Prefer jobs:write over role checks
  3. Provide fallbacks - Show appropriate UI when access is denied
  4. Handle loading states - Don't flash content during auth checks
// ✅ Good - Specific permission with fallback
<HasPermission permission="jobs:write" fallback={<ViewOnlyMessage />}>
  <EditButton />
</HasPermission>
 
// ❌ Avoid - Role check without fallback
<HasRole roles={["ADMIN"]}>
  <AdminButton />
</HasRole>