Skip to main content

Guards de rutas

src/app/core/guards/ contiene cuatro guards canActivate. Tres son clases (AuthGuard, AdminGuard, LoggedInAuthGuard, todas providedIn: 'root') y uno es una función (permissionGuard, CanActivateFn). No hay canMatch, canDeactivate, canActivateChild ni resolvers en el proyecto.

Contexto relacionado: Auth, interceptores y RBAC (modelo de permisos), Rutas y layouts (árbol de rutas) y Interceptores (el otro mecanismo que reacciona a una sesión caída).

Resumen​

GuardArchivoTipoPregunta que respondeSi falla
AuthGuardauth.guard.tsclase, síncrono¿hay un JWT vigente en el almacenamiento?userLogout + /auth/login (salvo iframe)
AdminGuardadmin.guard.tsclase, Observable<boolean>¿el userType del perfil es admin?/dashboard
permissionGuardpermission.guard.tsfunción, Observable<boolean>¿el acrónimo de data.permission está en el conjunto del usuario?/profile/profile-detail (ruta inexistente, ver gotchas)
LoggedInAuthGuardloggedIn.guard.tsclase, síncrono¿el usuario ya está logueado? (guard inverso)/

AuthGuard​

canActivate(): boolean
  1. AuthService.isLoggedOut() es !isLoggedIn(). isLoggedIn() lee USER_SESSION de secureStorage, decodifica el JWT con StorageService.parseJwt y compara exp * 1000 con la hora actual. Sin sesión guardada devuelve false.
  2. Si la sesión no está vencida, devuelve true.
  3. Si está vencida y IframeDetectionService.isIframe() es true, devuelve true igualmente: en modo embebido se espera el handshake postMessage (ver Frame bridge) en lugar de expulsar al usuario.
  4. En cualquier otro caso despacha UserActions.userLogout(), ejecuta router.navigate(['/auth/login']) y devuelve false.

Solo valida expiración local: no verifica la firma del JWT ni consulta al backend. Un token revocado pasa el guard y la primera petición HTTP devolverá 401, que resuelve el interceptor de errores.

Dónde se usa: shared/routes/content.routes.ts en todas las rutas hijas del ContentComponent (con o sin permissionGuard) y, de nuevo, en features/settings/settings-routing.module.ts. Las rutas bajo FullComponent (auth) no lo llevan.

Spec: auth.guard.spec.ts (5 casos: permite con sesión, bloquea y redirige a /auth/login sin sesión, despacha userLogout, no redirige si hay sesión). El caso de iframe no tiene spec; el test lo simula con isIframe: () => false.

AdminGuard​

canActivate(): Observable<boolean>

Lee GlobalService.profile (un ReplaySubject<User>(1)), toma el primer valor (take(1)), normaliza user?.userType con toString().trim().toLowerCase() y lo compara con TYPES_USER_PERMISSIONS.ADMIN ('admin'). Si no coincide, ejecuta router.navigate(['/dashboard']) y emite false.

  • La comparación es insensible a mayúsculas: Admin también pasa (cubierto por spec).
  • Un perfil sin userType se trata como '' y se deniega.
  • Tipos como atc, agent, collaboratorAgent o collaboratorOperator se deniegan.

Dónde se usa: solo en features/settings/settings-routing.module.ts, junto a AuthGuard y permissionGuard (canActivate: [AuthGuard, AdminGuard, permissionGuard]).

Spec: admin.guard.spec.ts (6 casos, incluida la comparación sin distinguir mayúsculas y la redirección a /dashboard).

:::caution Espera el perfil ReplaySubject no emite hasta que get-me responde. Tras un refresco de página el guard queda pendiente hasta que loadUserSession carga el perfil. Si el perfil nunca llega (por ejemplo, iframe sin sesión, donde AuthGuard deja pasar), la navegación a settings queda colgada en lugar de denegarse. :::

permissionGuard​

export const permissionGuard: CanActivateFn = (route) => Observable<boolean>

Lee route.data['permission'] (un único acrónimo, p. ej. 'CONTACT_READ') y llama a MyPermissionsService.hasPermission(acronym):

  • Si devuelve true, deja pasar.
  • Si devuelve false, ejecuta router.navigate(['/profile/profile-detail']) y emite false.
  • Si el observable lanza un error (catchError), hace lo mismo que en la denegación: redirige y emite false. Así un fallo al resolver permisos nunca deja el router con la pantalla en blanco.

MyPermissionsService resuelve contra una caché con TTL de 10 s (PERMISSIONS_TTL_MS, ver Auth y RBAC): si el conjunto está obsoleto vuelve a llamar a getMeUser(), y si esa llamada falla queda en conjunto vacío (todo denegado).

Detalles de contrato:

  • Solo admite un acrónimo. No hay variante "cualquiera de" para rutas; esa semántica existe solo en la directiva *appShowWithPermission (ver Directivas).
  • Si una ruta olvida declarar data.permission, route.data['permission'] es undefined, hasPermission(undefined) devuelve false y la ruta se deniega siempre.

Rutas protegidas​

El guard se usa en dos niveles. En content.routes.ts va combinado con AuthGuard, y los módulos lazy repiten permissionGuard (con el mismo acrónimo) en sus propios *-routing.module.ts. La segunda comprobación es casi gratuita gracias a la caché.

Ruta (PAGE_URLS)Guards en content.routes.tsdata.permission
dashboardAuthGuardsin permiso
whaticketAuthGuardsin permiso en la ruta raíz; whaticket/chat exige TICKET_READ en su módulo
internal-chatAuthGuardsin permiso en la raíz; internal-chat/conversations exige INTERNAL_CHAT_READ en su módulo
profileAuthGuardsin permiso
access-control/roles, permissions, role-permissions, modules, permissions-builderAuthGuard, permissionGuardACCESS_CONTROL_READ
labelsAuthGuard, permissionGuardLABEL_READ
scheduled-messagesAuthGuard, permissionGuardMESSAGE_READ (reutiliza el acrónimo de mensajes)
locked-ticketsAuthGuard, permissionGuardTICKET_UNLOCK_ANY
quick-responsesAuthGuard, permissionGuardQUICK_RESPONSE_READ
contactsAuthGuard, permissionGuardCONTACT_READ
usersAuthGuard, permissionGuardUSER_READ
departmentsAuthGuard, permissionGuardDEPARTMENT_READ
connectionsAuthGuard, permissionGuardCONNECTION_READ
settingsAuthGuard, permissionGuard (y AdminGuard en el módulo)SETTINGS_READ

Rutas sin protección de sesión: auth/login (solo LoggedInAuthGuard) y auth/reconnect (ninguna).

Spec: permission.guard.spec.ts (4 casos con TestBed.runInInjectionContext: permite, deniega, redirige a /profile/profile-detail, no redirige al permitir). No hay spec del caso catchError.

LoggedInAuthGuard​

canActivate(): boolean

Guard inverso: si AuthService.isLoggedIn() es true, ejecuta router.navigateByUrl('/') y devuelve false; en caso contrario devuelve true. Se usa únicamente en features/auth/auth.module.ts sobre la ruta login, para que un usuario con sesión vigente no vea el formulario. / redirige a whaticket/chat (ver Rutas).

Spec: loggedIn.guard.spec.ts (4 casos).

Flujo de decisión​

Gotchas​

  1. Destino de redirección inexistente. permissionGuard envía a /profile/profile-detail, pero ProfileRoutingModule solo define '' y starred-messages; no existe profile-detail. Por lectura del árbol de rutas (no ejecutado), esa URL cae en el comodín ** (redirige a auth/login) y, como el usuario está logueado, LoggedInAuthGuard lo manda a / y de ahí a whaticket/chat. Un usuario sin TICKET_READ podría entrar en un ciclo de redirecciones, porque whaticket/chat también exige permiso. La spec solo comprueba que se llame a navigate con esa cadena, no que la ruta exista.
  2. Doble guard con efectos laterales. Los guards canActivate de una misma ruta se subscriben a la vez y el router prioriza el resultado por orden en el array (comportamiento de prioritizedGuardValue; no verificado en ejecución). Por eso AdminGuard o permissionGuard pueden llamar a router.navigate aunque AuthGuard ya haya devuelto false.
  3. Solo expiración local. AuthGuard y LoggedInAuthGuard confían en exp; el backend sigue siendo la autoridad (401 o 403).
  4. Menú y rutas pueden divergir. El sidebar oculta ítems con Menu.permission mediante *appShowWithPermission, pero un enlace directo a la URL sigue pasando por permissionGuard. Y al revés: dashboard, profile y whaticket/internal-chat (a nivel raíz) no declaran permiso en content.routes.ts.
  5. Caché de 10 s. Un rol modificado en el servidor puede tardar hasta 10 s en reflejarse; el 403 resultante se muestra como toast (ver Interceptores).