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ódulo | Página | Entrada de menú |
|---|---|---|---|
access-control/roles | RolesModule | RolesPageComponent | ac-roles |
access-control/permissions | PermissionsModule | PermissionsPageComponent | ac-permissions |
access-control/role-permissions | RolePermissionsModule | RolePermissionsPageComponent | ac-role-permissions |
access-control/modules | ModulesModule | ModulesPageComponent | ac-modules |
access-control/permissions-builder | PermissionsBuilderModule | PermissionsBuilderPageComponent | ac-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ónimo | Efecto en la UI | Backend (@RequirePermission) |
|---|---|---|
ACCESS_CONTROL_READ | Acceso 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_CREATE | Botón "agregar" de cada tabla (*appShowWithPermission) | create |
ACCESS_CONTROL_UPDATE | Botón "editar" por fila y botón "guardar" del constructor | update/:id |
ACCESS_CONTROL_DELETE | Botón "eliminar" por fila | remove/: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/:
| Interfaz | Archivo | Campos principales |
|---|---|---|
SystemModule / SystemModuleForm | system-module.interface.ts | _id, identifier, name, acronym, description, isActive, createdAt, updatedAt |
Permission / PermissionForm | permission.interface.ts | los anteriores más moduleId: string | SystemModule |
CrmRole / CrmRoleForm | crm-role.interface.ts | los anteriores más tenantId |
CrmRolePermission / CrmRolePermissionForm | crm-role-permission.interface.ts | name, acronym, roleId: string | CrmRole, permissions: string[] | Permission[], tenantId |
ModuleBoard | permissions-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.
| Servicio | Base | Operaciones |
|---|---|---|
SystemModuleService | /modules | GET /get-all, GET /get-one/:identifier, POST /create, PATCH /update/:identifier, DELETE /remove/:identifier |
PermissionService | /permissions | mismas cinco operaciones |
CrmRoleService | /roles | mismas cinco operaciones |
CrmRolePermissionService | /role-permissions | mismas 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.
| Tabla | Columnas | Datos adicionales cargados |
|---|---|---|
RolesTableComponent | nombre, acrónimo, descripción, activo, creado, acciones | ninguno |
ModulesTableComponent | idem (claves systemModules.fields.*) | ninguno |
PermissionsTableComponent | nombre, acrónimo, descripción, módulo, activo, creado, acciones | SystemModuleService.getAll() para resolver el nombre del módulo y pasarlo al modal |
RolePermissionsTableComponent | nombre, acrónimo, rol, cantidad de permisos, activo, creado, acciones | roles (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.
| Campo | Validadores | Aplica a |
|---|---|---|
name | required, maxLength(100) | los cuatro |
acronym | required, maxLength(20) | los cuatro |
description | maxLength(500); se envía undefined si está vacía | los cuatro |
isActive | ninguno, por defecto true (checkbox) | los cuatro |
moduleId | required; ng-select con bindValue="_id", bindLabel="name" | permisos |
roleId | required; ng-select simple | role-permissions |
permissions | required; 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.
- Al iniciar hace
forkJoindeCrmRoleService.getAll,PermissionService.getAll,SystemModuleService.getAllyCrmRolePermissionService.getAll, y se queda solo con roles, permisos y módulos conisActive. - Un
ng-selectelige 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 enavailableyassignedsegún la asignaciónCrmRolePermissiondel rol. Módulos sin permisos no aparecen. - 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.
- Se arrastra con
@angular/cdk/drag-drop(cdkDropList,cdkDrag,cdkDragHandle). Cada listaavailable-<moduleId>está conectada solo conassigned-<moduleId>: no se puede mover un permiso a otro módulo.drop()usamoveItemInArray/transferArrayItem. save()(visible conACCESS_CONTROL_UPDATE) junta los ids de todos losassigned. Si el rol ya tieneCrmRolePermission, llamaupdate(identifier, { permissions }); si no,createconname: "<rol> - Permisos",acronymdel rol,roleId,permissionsytenantIddel 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
- Crear módulo (sección) en
modules. - Crear permisos del módulo en
permissions(el acrónimo es el que se evalúa endata.permissiony*appShowWithPermission). - Crear el rol en
roles. - Asignar permisos al rol en
role-permissions(multiselect) o enpermissions-builder(arrastrar). - 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
RoleServicehuérfano:core/services/api/role.service.tsapunta a/role/create,/role/find,/role/update(rutas del modelo anterior de roles con árbol de permisos, vercore/interfaces/api/role.interface.ts). Ningún componente lo importa; la feature usaCrmRoleService.- Mismo permiso para todo: no existen permisos específicos por recurso (
ROLE_*,PERMISSION_*); un usuario conACCESS_CONTROL_UPDATEpuede modificar roles, permisos, módulos y asignaciones. - Una asignación por rol en el constructor:
_activeRolePermissiontoma el primerCrmRolePermissioncuyoroleIdcoincide; 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
permissionGuardademás del guard de la ruta padre; si se deniega, navega a/profile/profile-detail(ver nota en Rutas).