Skip to main content

Control de acceso (features/access-control)

Administración del modelo RBAC desde la UI: módulos del sistema, permisos, roles, asignaciones rol-permisos y un constructor visual de permisos (arrastrar y soltar). Contraparte del backend: RBAC. Cómo el frontend consume los permisos resueltos está en Auth, interceptores y RBAC.

Estructura​

La feature no tiene módulo raíz: son cinco módulos lazy independientes bajo access-control/features/<x>, cada uno con su <x>.module.ts, <x>-routing.module.ts, una página y (salvo el constructor) una tabla y un modal-formulario. Siguen el patrón CRUD de Módulos de negocio, sin componente de filtro.

access-control/features/
├── roles/ # RolesTable, RoleModalForm
├── permissions/ # PermissionsTable, PermissionModalForm
├── modules/ # ModulesTable, ModuleModalForm
├── role-permissions/ # RolePermissionsTable, RolePermissionModalForm
└── permissions-builder/ # PermissionsBuilderPage (sin tabla ni modal)

Rutas​

Se registran en shared/routes/content.routes.ts con canActivate: [AuthGuard, permissionGuard] y data: { permission: 'ACCESS_CONTROL_READ' }. Cada *-routing.module.ts repite permissionGuard con el mismo permiso en su ruta vacía (doble verificación).

Ruta (PAGE_URLS)MóduloPáginaEntrada de menú
access-control/rolesRolesModuleRolesPageComponentac-roles
access-control/permissionsPermissionsModulePermissionsPageComponentac-permissions
access-control/role-permissionsRolePermissionsModuleRolePermissionsPageComponentac-role-permissions
access-control/modulesModulesModuleModulesPageComponentac-modules
access-control/permissions-builderPermissionsBuilderModulePermissionsBuilderPageComponentac-permissions-builder

El grupo access-control del menú (core/helpers/ui/nav.constant.ts) es un sub con permission: 'ACCESS_CONTROL_READ' y cinco hijos con el mismo permiso. No existe una ruta access-control sin sufijo.

Permisos requeridos​

Un solo módulo de permisos gobierna toda la feature:

AcrónimoEfecto en la UIBackend (@RequirePermission)
ACCESS_CONTROL_READAcceso a las cinco rutas y al grupo del menúget-all y get-one/:id de roles, permisos, módulos y role-permissions
ACCESS_CONTROL_CREATEBotón "agregar" de cada tabla (*appShowWithPermission)create
ACCESS_CONTROL_UPDATEBotón "editar" por fila y botón "guardar" del constructorupdate/:id
ACCESS_CONTROL_DELETEBotón "eliminar" por filaremove/:id

El botón "ver" (BUTTON_ACTIONS.VIEW) no tiene restricción adicional: basta con poder entrar a la ruta. El backend aplica los mismos acrónimos con el decorador @RequirePermission en los controladores module, permission, role y role-permission (verificado en el código fuente del backend); ese decorador y su guard aún no están descritos en las páginas de Decoradores y Guards.

Modelos​

Definidos en core/interfaces/api/:

InterfazArchivoCampos principales
SystemModule / SystemModuleFormsystem-module.interface.ts_id, identifier, name, acronym, description, isActive, createdAt, updatedAt
Permission / PermissionFormpermission.interface.tslos anteriores más moduleId: string | SystemModule
CrmRole / CrmRoleFormcrm-role.interface.tslos anteriores más tenantId
CrmRolePermission / CrmRolePermissionFormcrm-role-permission.interface.tsname, acronym, roleId: string | CrmRole, permissions: string[] | Permission[], tenantId
ModuleBoardpermissions-builder-page.interface.ts{ module, available: Permission[], assigned: Permission[] }

moduleId, roleId y permissions pueden llegar como id o como objeto poblado; los componentes lo normalizan con typeof x === 'object'.

Servicios y endpoints​

Todos son providedIn: 'root' y usan environment.apiUrl más API_URLS y API_ENDPOINTS (core/helpers/global/global.constant.ts). Las respuestas vienen envueltas en ApiResponse; las listas en data.results.

ServicioBaseOperaciones
SystemModuleService/modulesGET /get-all, GET /get-one/:identifier, POST /create, PATCH /update/:identifier, DELETE /remove/:identifier
PermissionService/permissionsmismas cinco operaciones
CrmRoleService/rolesmismas cinco operaciones
CrmRolePermissionService/role-permissionsmismas cinco operaciones

getAll() no pagina: pide get-all sin parámetros y devuelve data.results o []. Las tablas usan paginación local del shared-tables-ngx-datatable (externalSorting: false, showFooter: true).

Las URLs de get-one, update y remove usan el campo identifier del registro (no _id). Los toasts de éxito los emiten SystemModuleService, PermissionService y CrmRolePermissionService (systemModules.created, permissions.updated, ...); en roles los emite el modal (roles.created / roles.updated) porque CrmRoleService no notifica. La tabla de roles no muestra toast al eliminar (el servicio no notifica y la tabla solo recarga).

:::caution Backend con rutas planas La página RBAC describe GET /, POST /, PATCH /:id. El frontend (y los controladores del backend, vía CRUD_ROUTES) usan get-all, get-one/:id, create, update/:id y remove/:id; ver la nota al final de Endpoints. :::

Tablas y modales​

Cada tabla (<x>-table.component.ts) carga en ngOnInit, construye tableConfig en ngAfterViewInit (las columnas usan TemplateRef obtenidos con @ViewChild), muestra un botón de recargar y, con permiso, el de agregar. Los modales se abren con NgbModal.open(..., { backdrop: 'static', centered: true, keyboard: false, size: 'lg' }), reciben action (ADD, EDIT, VIEW) y data, y la tabla recarga en closed. El borrado usa swalWithBootstrapButtons (SweetAlert) con textos <x>.modal.delete, btn.confirm y btn.cancel; al confirmar llama remove(identifier) y recarga.

TablaColumnasDatos adicionales cargados
RolesTableComponentnombre, acrónimo, descripción, activo, creado, accionesninguno
ModulesTableComponentidem (claves systemModules.fields.*)ninguno
PermissionsTableComponentnombre, acrónimo, descripción, módulo, activo, creado, accionesSystemModuleService.getAll() para resolver el nombre del módulo y pasarlo al modal
RolePermissionsTableComponentnombre, acrónimo, rol, cantidad de permisos, activo, creado, accionesroles (CrmRoleService) y permisos (PermissionService) para el modal

La columna "activo" pinta un badge con los textos fijos "Activo" / "Inactivo" (sin translate).

Formularios y validaciones​

Los cuatro modales son FormGroup reactivos con la misma base; en VIEW el formulario se deshabilita y se oculta el pie; isSubmitting evita doble envío y el botón se deshabilita con invalid || isSubmitting.

CampoValidadoresAplica a
namerequired, maxLength(100)los cuatro
acronymrequired, maxLength(20)los cuatro
descriptionmaxLength(500); se envía undefined si está vacíalos cuatro
isActiveninguno, por defecto true (checkbox)los cuatro
moduleIdrequired; ng-select con bindValue="_id", bindLabel="name"permisos
roleIdrequired; ng-select simplerole-permissions
permissionsrequired; ng-select con [multiple]="true"; por defecto []role-permissions

Los errores se muestran con el texto genérico words.fieldRequired (también para maxlength, que además se limita con el atributo HTML). Ningún formulario envía tenantId: lo resuelve el backend.

Constructor de permisos​

PermissionsBuilderPageComponent (permissions-builder/pages/permissions-builder-page) es una alternativa visual a role-permissions.

  1. Al iniciar hace forkJoin de CrmRoleService.getAll, PermissionService.getAll, SystemModuleService.getAll y CrmRolePermissionService.getAll, y se queda solo con roles, permisos y módulos con isActive.
  2. Un ng-select elige el rol (selectedRoleId). onRoleChange() reinicia la búsqueda y los colapsos y arma el tablero (_buildBoard): por cada módulo activo con permisos, separa los permisos del módulo en available y assigned según la asignación CrmRolePermission del rol. Módulos sin permisos no aparecen.
  3. Dos columnas (disponibles / asignados) agrupan por módulo, con contador, colapso por grupo y botones "asignar todos" / "quitar todos". La búsqueda filtra por nombre o acrónimo; mientras se busca no se puede colapsar.
  4. Se arrastra con @angular/cdk/drag-drop (cdkDropList, cdkDrag, cdkDragHandle). Cada lista available-<moduleId> está conectada solo con assigned-<moduleId>: no se puede mover un permiso a otro módulo. drop() usa moveItemInArray / transferArrayItem.
  5. save() (visible con ACCESS_CONTROL_UPDATE) junta los ids de todos los assigned. Si el rol ya tiene CrmRolePermission, llama update(identifier, { permissions }); si no, create con name: "<rol> - Permisos", acronym del rol, roleId, permissions y tenantId del rol. Actualiza la caché local con la respuesta.

El <h3> de la página usa permissionsBuilder.title y no el shared-breadcrumb de las otras páginas.

Estado, sockets y utilidades​

No hay slice NgRx ni sockets: cada pantalla guarda su lista en el componente. Tras cambiar roles o permisos, el menú y las directivas siguen usando el Set de MyPermissionsService hasta que caduque su TTL o se recargue get-me (ver Auth, interceptores y RBAC). Utilidades compartidas: DEFAULT_NGX_DATATABLE_CONFIG, BUTTON_ACTIONS, FORMAT_FOR_DATES y swalWithBootstrapButtons (ver Compartido).

Flujo típico​

  1. Crear módulo (sección) en modules.
  2. Crear permisos del módulo en permissions (el acrónimo es el que se evalúa en data.permission y *appShowWithPermission).
  3. Crear el rol en roles.
  4. Asignar permisos al rol en role-permissions (multiselect) o en permissions-builder (arrastrar).
  5. Asignar el rol al usuario en Usuarios.

Pruebas​

Solo modules-table.component.spec.ts existe en esta feature (comprueba que loadModules se invoca al iniciar). El resto de tablas, modales y el constructor no tienen specs.

Gotchas e inconsistencias​

  • RoleService huérfano: core/services/api/role.service.ts apunta a /role/create, /role/find, /role/update (rutas del modelo anterior de roles con árbol de permisos, ver core/interfaces/api/role.interface.ts). Ningún componente lo importa; la feature usa CrmRoleService.
  • Mismo permiso para todo: no existen permisos específicos por recurso (ROLE_*, PERMISSION_*); un usuario con ACCESS_CONTROL_UPDATE puede modificar roles, permisos, módulos y asignaciones.
  • Una asignación por rol en el constructor: _activeRolePermission toma el primer CrmRolePermission cuyo roleId coincide; si hay varios para el mismo rol, solo uno se edita.
  • Nombre fijo al crear desde el constructor: la asignación nueva se llama "<rol> - Permisos", con texto en español no traducible.
  • Roles inactivos: el constructor solo lista roles con isActive; el modal de role-permissions recibe todos los roles de la tabla, activos o no.
  • Las rutas de módulo repiten permissionGuard además del guard de la ruta padre; si se deniega, navega a /profile/profile-detail (ver nota en Rutas).