Skip to main content

Overview

SIGEAC implements a comprehensive Role-Based Access Control (RBAC) system where users are assigned roles that determine their access to features, modules, and data. The system supports hierarchical roles, permission inheritance, and company-specific role assignments.

Role Structure

Role Type Definition

types/index.ts
export type Role = {
  id: number;
  name: string;           // Role identifier (e.g., "SUPERUSER")
  label: string;          // Human-readable label (e.g., "Super Usuario")
  company: {              // Roles can be company-specific
    name: string;
    description: string;
  }[];
};

export type Permission = {
  id: number;
  name: string;           // Permission identifier
  label: string;          // Human-readable label
  modules: {              // Permissions are tied to modules
    id: number;
    name: string;
    description: string;
    registered_by: string;
    company_id: string;
    company: {
      id: number;
      name: string;
      description: string;
    };
  }[];
};

export type User = {
  id: string;
  username: string;
  first_name: string;
  last_name: string;
  email: string;
  isActive: boolean;
  roles?: {                          // Users can have multiple roles
    id: number;
    name: string;
    label: string;
    permissions: Permission[];       // Each role has permissions
  }[];
  permissions: Permission[];         // Direct user permissions
  companies: Company[];
  employee: Employee[];
};

System Roles

SIGEAC has a hierarchical role system organized by department and seniority:

Administrative Roles

SUPERUSER
  • Full system access across all companies
  • Bypass most access restrictions
  • Access to system configuration
  • Special handling in session management (no auto-logout)
Role Check Example
const isSuperUser = user?.roles?.some(
  (role) => role.name === 'SUPERUSER'
);

Department Roles

SIGEAC organizes roles by aviation departments:

Warehouse (Almacén)

Warehouse Roles
[
  "JEFE_ALMACEN",          // Warehouse Manager
  "ANALISTA_ALMACEN",      // Warehouse Analyst
]
Permissions:
  • Inventory management
  • Article dispatch
  • Stock control
  • Tool calibration tracking
  • Restock requests

SMS (Safety Management System)

SMS Roles
[
  "JEFE_SMS",              // SMS Manager
  "ANALISTA_SMS",          // SMS Analyst
]
Permissions:
  • Voluntary reports
  • Obligatory reports
  • Hazard identification
  • Mitigation plans
  • Safety bulletins
  • Survey management

Planning (Planificación)

Planning Roles
[
  "JEFE_PLANIFICACION",         // Planning Manager
  "ANALISTA_PLANIFICACION",     // Planning Analyst
]
Permissions:
  • Work order creation
  • Maintenance scheduling
  • Aircraft part tracking
  • Flight control logs
  • Service task management

Procurement (Compras)

Procurement Roles
[
  "JEFE_COMPRAS",          // Procurement Manager
  "ANALISTA_COMPRAS",      // Procurement Analyst
]
Permissions:
  • Purchase orders
  • Quote requests
  • Vendor management
  • Requisition approvals

Administration (Administración)

Administration Roles
[
  "JEFE_ADMINISTRACION",         // Administration Manager
  "ANALISTA_ADMINISTRACION",     // Administration Analyst
  "CONTADOR_ADMINISTRACION",     // Accountant
  "RRHH_ADMINISTRACION",         // HR Administrator
]
Permissions:
  • Financial operations
  • Cash flow management
  • Flight operations
  • Employee management
  • Client/vendor relations

Development (Desarrollo)

Development Roles
[
  "JEFE_DESARROLLO",       // Development Manager
  "ANALISTA_DESARROLLO",   // Development Analyst
]
Permissions:
  • Activity reports
  • Project tracking
  • Development tasks

Accounting (Contaduría)

Accounting Roles
[
  "JEFE_CONTADURIA",       // Accounting Manager
]

Role-Based Routing

Dashboard Selection

SIGEAC routes users to different dashboards based on their roles:
app/[company]/dashboard/page.tsx
export default function DashboardPage() {
  const { selectedCompany, selectedStation } = useCompanyStore();
  const { user, loading } = useAuth();

  if (loading) return <LoadingPage />;
  if (!user) return <DefaultDashboard />;

  const roleNames = user.roles?.map((r) => r.name) || [];

  const hasRole = (names: string[]) =>
    names.some((r) => roleNames.includes(r));

  const getDashboardType = () => {
    // Priority order matters!
    if (hasRole(["SUPERUSER"])) {
      return "SUPERUSER";
    }

    if (hasRole(["JEFE_ALMACEN", "ANALISTA_ALMACEN"])) {
      return "WAREHOUSE";
    }

    if (hasRole(["JEFE_SMS", "ANALISTA_SMS"])) {
      return "SMS";
    }

    if (
      hasRole([
        "ANALISTA_ADMINISTRACION",
        "RRHH_ADMINISTRACION",
        "JEFE_ADMINISTRACION",
        "CONTADOR_ADMINISTRACION",
      ])
    ) {
      return "ADMINISTRATION";
    }

    return "DEFAULT";
  };

  switch (getDashboardType()) {
    case "SUPERUSER":
      return <SuperUserDashboard companySlug={selectedCompany?.slug} />;
    
    case "WAREHOUSE":
      return <WarehouseDashboard companySlug={selectedCompany?.slug} />;
    
    case "SMS":
      return <SMSDashboard companySlug={selectedCompany?.slug} />;
    
    case "ADMINISTRATION":
      return <AdministrationDashboard companySlug={selectedCompany?.slug} />;
    
    default:
      return <DefaultDashboard companySlug={selectedCompany?.slug} />;
  }
}
Priority-Based Routing: If a user has multiple roles (e.g., both SUPERUSER and JEFE_ALMACEN), the dashboard with higher priority is shown. SUPERUSER always takes precedence.

Protected Routes

Layout-Level Protection

Each module has a protected layout that enforces role requirements:
app/[company]/almacen/layout.tsx
import ProtectedLayout from '@/components/layout/ProtectedLayout';

const WarehouseLayout = ({ children }: { children: React.ReactNode }) => {
  return (
    <ProtectedLayout roles={["ANALISTA_ALMACEN", "JEFE_ALMACEN", "SUPERUSER", "REGULAR"]}>
      {children}
    </ProtectedLayout>
  );
}

export default WarehouseLayout;

ProtectedLayout Component

components/layout/ProtectedLayout.tsx
interface ProtectedLayoutProps {
  children: ReactNode;
  roles?: string[];           // Required roles
  permissions?: string[];     // Required permissions
}

const ProtectedLayout = ({ children, roles, permissions }: ProtectedLayoutProps) => {
  const { user, loading } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (!loading && !user) {
      router.push('/login');
    }
  }, [loading, user, router]);

  useEffect(() => {
    if (!loading && user) {
      const userRoles = user.roles?.map(role => role.name) || [];
      const userPermissions = user.roles?.flatMap(role =>
        role.permissions.map(permission => permission.name)
      ) || [];

      // Check role access
      if (roles && !roles.some(role => userRoles.includes(role))) {
        router.push('/not-authorized');
        return;
      }

      // Check permission access
      if (permissions && !permissions.some(permission => userPermissions.includes(permission))) {
        router.push('/not-authorized');
      }
    }
  }, [loading, user, roles, permissions, router]);

  if (loading) return <LoadingPage />;
  if (!user) return null;

  // Final verification before render
  const finalUserRoles = user.roles?.map(role => role.name) || [];
  const finalUserPermissions = user.roles?.flatMap(role =>
    role.permissions.map(permission => permission.name)
  ) || [];

  if (roles && !roles.some(role => finalUserRoles.includes(role))) {
    return null;
  }

  if (permissions && !permissions.some(permission => finalUserPermissions.includes(permission))) {
    return null;
  }

  return <>{children}</>;
};
Double Verification: The ProtectedLayout performs role checks both in useEffect (for navigation) and before render (for security). This prevents unauthorized content from flashing on screen.
Navigation menus are filtered based on user roles:
lib/menu-list.tsx
type Menu = {
  href: string;
  label: string;
  active: boolean;
  icon: LucideIcon;
  roles: string[];        // Required roles to see this menu
  submenus: Submenu[];
};

type Submenu = {
  href: string;
  label: string;
  active: boolean;
  roles?: string[];       // Optional: inherit from parent if not specified
};

export function getMenuList(
  pathname: string,
  company: string,
  userRoles: string[]
): Group[] {
  function hasAccess(menuItem: Menu | Submenu): boolean {
    if (!menuItem.roles || menuItem.roles.length === 0) {
      return true;  // No role restriction
    }
    return menuItem.roles.some((role) => userRoles.includes(role));
  }
  
  // Example: SMS Menu
  return [
    {
      groupLabel: "SMS",
      menus: [
        {
          href: "/transmandu/sms",
          label: "Reportes",
          icon: ClipboardPen,
          roles: ["ANALISTA_SMS", "JEFE_SMS", "SUPERUSER"],
          submenus: [
            {
              href: "/transmandu/sms/reportes/reportes_voluntarios",
              label: "Reportes Voluntarios",
              roles: ["ANALISTA_SMS", "JEFE_SMS", "SUPERUSER"],
            },
            {
              href: "/transmandu/sms/reportes/reportes_obligatorios",
              label: "Reportes Obligatorios",
              roles: ["ANALISTA_SMS", "JEFE_SMS", "SUPERUSER"],
            },
          ],
        },
      ],
    },
  ].map(group => ({
    ...group,
    menus: group.menus.filter(hasAccess).map(menu => ({
      ...menu,
      submenus: menu.submenus.filter(hasAccess)
    }))
  }));
}

Usage in Components

components/sidebar/Menu.tsx
const { user } = useAuth();
const { selectedCompany } = useCompanyStore();

const userRoles = user?.roles?.map(r => r.name) || [];
const menuList = useMemo(() => {
  return getMenuList(pathname, selectedCompany?.name, userRoles);
}, [pathname, selectedCompany, userRoles]);

Permission Checks

For granular access control, use permission-level checks:
Example Permission Check
const { user } = useAuth();

const userPermissions = user?.roles?.flatMap(role =>
  role.permissions.map(p => p.name)
) || [];

const canCreateWorkOrder = userPermissions.includes('create_work_order');
const canApproveRequisition = userPermissions.includes('approve_requisition');

if (!canCreateWorkOrder) {
  return <UnauthorizedMessage />;
}

Role Checking Utilities

Create reusable hooks for role checking:
hooks/useRoleCheck.ts
import { useAuth } from '@/contexts/AuthContext';

export function useRoleCheck() {
  const { user } = useAuth();
  
  const roleNames = user?.roles?.map(r => r.name) || [];
  
  const hasRole = (roles: string | string[]) => {
    const roleArray = Array.isArray(roles) ? roles : [roles];
    return roleArray.some(role => roleNames.includes(role));
  };
  
  const hasAllRoles = (roles: string[]) => {
    return roles.every(role => roleNames.includes(role));
  };
  
  const hasPermission = (permissionName: string) => {
    const permissions = user?.roles?.flatMap(role =>
      role.permissions.map(p => p.name)
    ) || [];
    return permissions.includes(permissionName);
  };
  
  return {
    user,
    roleNames,
    hasRole,
    hasAllRoles,
    hasPermission,
    isSuperUser: hasRole('SUPERUSER'),
    isWarehouseUser: hasRole(['JEFE_ALMACEN', 'ANALISTA_ALMACEN']),
    isSMSUser: hasRole(['JEFE_SMS', 'ANALISTA_SMS']),
  };
}

Usage Example

Component Using Role Check
import { useRoleCheck } from '@/hooks/useRoleCheck';

export default function ArticleDispatchPage() {
  const { hasRole, isSuperUser, isWarehouseUser } = useRoleCheck();
  
  if (!isWarehouseUser && !isSuperUser) {
    return <UnauthorizedPage />;
  }
  
  return (
    <div>
      <h1>Article Dispatch</h1>
      {hasRole('JEFE_ALMACEN') && (
        <button>Approve All</button>
      )}
    </div>
  );
}

Module-Based Access

Access is also controlled by company modules:
Module Filtering
const isModuleActive = (moduleValue: string) => {
  return selectedCompany?.modules?.some(
    (module) => module.value === moduleValue
  );
};

const hasRoleAccess = (menuItem: { roles?: string[] }): boolean => {
  if (!menuItem.roles || menuItem.roles.length === 0) return true;
  return menuItem.roles.some(role => userRoles.includes(role));
};

// Filter menus by both module AND role
const filteredMenus = menus.filter(
  (menu) => isModuleActive(menu.moduleValue) && hasRoleAccess(menu)
);

Best Practices

Wrap all protected pages with ProtectedLayout to enforce server-side and client-side checks:
<ProtectedLayout roles={["JEFE_ALMACEN", "ANALISTA_ALMACEN", "SUPERUSER"]}>
  <YourProtectedPage />
</ProtectedLayout>
Unless explicitly restricted, always include SUPERUSER in role arrays to ensure admins have access:
roles={["ANALISTA_SMS", "JEFE_SMS", "SUPERUSER"]}
For maximum security, verify roles in:
  • Middleware (server)
  • Layout components (client)
  • API endpoints (server)
Server Action
export async function createWorkOrder(data: FormData) {
  const session = await verifySession();
  const user = await getUserById(session.userId);
  
  const hasAccess = user.roles.some(r => 
    ['JEFE_PLANIFICACION', 'ANALISTA_PLANIFICACION', 'SUPERUSER'].includes(r.name)
  );
  
  if (!hasAccess) {
    throw new Error('Unauthorized');
  }
  
  // Proceed with work order creation...
}
For specific operations, check permissions instead of roles:
<ProtectedLayout permissions={["approve_purchase_orders"]}>
  <PurchaseOrderApprovalPage />
</ProtectedLayout>

Common Access Patterns

Allow users to view but not modify:
const { hasRole } = useRoleCheck();
const canEdit = hasRole(['JEFE_ALMACEN', 'SUPERUSER']);

<Button disabled={!canEdit}>Edit Article</Button>

Build docs developers (and LLMs) love