Autenticación (features/auth)
Pantallas públicas de acceso: el login con email y contraseña y la pantalla de reconexión que se muestra cuando el CRM corre dentro de un iframe y la sesión caduca. La feature solo contiene UI; la lógica de sesión vive en NgRx y en AuthService (ver Auth, interceptores y RBAC y Frame bridge). Contraparte del backend: Auth.
Rutas
AuthModule (features/auth/auth.module.ts) se carga con loadChildren bajo auth desde shared/routes/full.routes.ts, es decir dentro de FullComponent (layout sin header ni sidebar; ver Rutas).
| Ruta | Componente | Guard | Permiso |
|---|---|---|---|
auth/login | LoginComponent | LoggedInAuthGuard | ninguno |
auth/reconnect | ReconnectComponent | ninguno | ninguno |
auth (vacía) | redirige a login (pathMatch: 'full') | ninguno | ninguno |
LoggedInAuthGuard (core/guards/loggedIn.guard.ts) hace lo inverso a AuthGuard: si AuthService.isLoggedIn() es verdadero navega a / y bloquea el login; si no, lo deja pasar. /reconnect no tiene guard, de modo que es alcanzable con o sin sesión.
Archivos
| Archivo | Rol |
|---|---|
auth.module.ts | Declara los dos componentes; importa ReactiveFormsModule, RouterModule.forChild y TranslateModule |
pages/login/login.component.{ts,html,scss} | Formulario de login y panel decorativo |
pages/reconnect/reconnect.component.{ts,html,scss} | Aviso de sesión expirada y botón para pedir un nuevo handshake al padre |
pages/login/login.component.spec.ts | Pruebas del componente de login |
Login
Formulario y validaciones
LoginComponent construye un FormGroup con FormBuilder:
| Campo | Validadores | Mensaje (clave i18n) |
|---|---|---|
email | required, email | auth.errorMessages.emailIsRequired / auth.errorMessages.invalidEmail |
password | required | auth.errorMessages.passwordIsRequired |
Los mensajes se muestran solo cuando el control está invalid y touched. El botón de envío se deshabilita con form.invalid o mientras loading$ está activo (spinner y texto auth.loadingText). El botón del ojo (togglePassword) alterna el type del input entre password y text.
Flujo
onSubmit()no hace nada si el formulario es inválido; si es válido limpia el error local y despachaUserActions.userAuthenticationRequest({ request: this.form.value }). El payload es{ email, password }(UserLoginRequest).- El efecto
loginUser$(core/states/auth/auth.effects.ts) usaswitchMap: llamaAuthService.login(POST <apiUrl>/auth/login, con el headerskip), guarda{ token }ensecureStoragebajoUSER_SESSION, llamaUserService.getMeUser(GET <apiUrl>/users/get-me), reescribe la sesión con{ ...usuario, token }, navega a/whaticket/chaty emiteuserAuthenticationSuccess. - Si cualquiera de las dos llamadas falla, emite
userAuthenticationFailurecon el mensaje traducido (error.error.messageoerror.message; si no hay,words.unknownError). - El componente escucha
userAuthenticationFailureconActions+ofTypey muestra elmessageen un bloquerole="alert"encima del formulario. El error se limpia en el siguiente envío.
El 401 de /auth/login no pasa por el flujo de logout del ErrorInterceptor porque la petición lleva el header skip (ver tratamiento de errores).
Estado NgRx usado
selectLoading(core/states/auth/auth.selectors.ts) alimentaloading$.- El reducer
authReducerponeloading: trueenuserAuthenticationRequestyfalseen success o failure;messageguarda textos en inglés ('Authenticating user...','Authentication failed: ...') que son de estado interno, no de UI.
Reconexión (iframe)
Cuando el CRM está embebido y el backend responde 401, el ErrorInterceptor emite CRM_AUTH_EXPIRED al padre y navega a /auth/reconnect. También llega aquí cuando falla frameLogin$.
ReconnectComponentmuestra un candado, el título "Sesión expirada" y el texto "Tu sesión ha caducado. Haz clic en reconectar para volver a ingresar." Estos textos y "Reconectar" / "Reconectando…" están escritos en duro en español en el HTML, no pasan portranslate.onReconnect()limpiaerrory llamaFrameBridgeService.emit('CRM_AUTH_EXPIRED'): pide al padre un nuevo handshakepostMessage. No navega ni llama al backend por sí mismo.- Cuando el padre responde,
AuthEffects(suscrito aFrameBridgeService.auth$) despachaframeAuthenticateRequest; el efectoframeLogin$llamaAuthService.frameLogin(POST <apiUrl>/auth/frame-login, headerskip), guarda sesión, navega a/whaticket/chaty emiteCRM_AUTHENTICATEDal padre. Si falla, vuelve a navegar a/auth/reconnect. - El componente también escucha
userAuthenticationFailurey pinta el mensaje enreconnect__alert;loading$deshabilita el botón y muestra "Reconectando…".
Servicios y endpoints
| Servicio | Método | Endpoint | Nota |
|---|---|---|---|
AuthService (core/services/api/auth.service.ts) | login(request) | POST /auth/login | Respuesta ApiResponse<{ token }>; header skip |
AuthService | frameLogin(payload) | POST /auth/frame-login | Body { token, type: 'user', data: { externalId, email?, username?, firstname?, lastName?, userType?, roleId? } }; respuesta { token, identifier, externalId } |
UserService (core/services/api/user.service.ts) | getMeUser() | GET /users/get-me | Perfil con permisos y feature flags |
AuthService además expone logout() (borra USER_SESSION y navega a /auth/login), isLoggedIn() / isLoggedOut() (comparan exp del JWT con la hora actual) y utilidades getDeviceInfo, isMobile, getCurrentPosition que el login no usa.
Modelos
UserLoginRequest (core/interfaces/api/user.interface.ts): email, password y los opcionales lastLogin, lastSessionIp, TFA, device. El login solo envía email y password. FrameAuthPayload está definido en frame-bridge.service.ts.
Permisos
Ninguno: son rutas públicas. Tras el login, los permisos se cargan con get-me y gobiernan el resto de la aplicación.
Gotchas e inconsistencias
- "¿Olvidó su contraseña?" (
auth.losePassword) es un<a href="#">sin handler: no hay flujo de recuperación en el frontend. - "Recordarme": el checkbox
#rememberno está ligado al formulario ni a ninguna lógica; no cambia la persistencia de la sesión (siempresecureStorage). - Las tarjetas del panel derecho ("Crecimiento Mensual +24.8%", "Mensajes Activos 1,204") son maquetas con datos fijos, no métricas reales.
- Las acciones
enableTwoFactorAuthentication,disableTwoFactorAuthenticationy los estadospreAuthexisten enauth.actions.tsyauth.reducers.ts, pero esta feature no ofrece pantallas de 2FA. loginUser$usaswitchMap: un segundo envío mientras hay uno en vuelo cancela el primero (el botón ya se deshabilita conloading$, así que es solo defensivo).- El mensaje de fallo de login proviene del backend; si trae una clave i18n se traduce con
TranslateService.instant, si no se muestra tal cual.
Sockets y utilidades
Esta feature no abre sockets: los sockets de chat se conectan en ContentComponent una vez hay perfil (ver Sockets). Las utilidades que usa están en core: StorageService.secureStorage y parseJwt (ver Servicios), FrameBridgeService e IframeDetectionService (ver Frame bridge).
Pruebas
| Spec | Cubre |
|---|---|
features/auth/pages/login/login.component.spec.ts | Construcción del formulario, validez con email inválido o contraseña vacía, no despacha si es inválido, despacha userAuthenticationRequest con los valores, limpieza del error al enviar, togglePassword, error por userAuthenticationFailure, reflejo de loading$ y destrucción limpia |
core/guards/loggedIn.guard.spec.ts | Permite el login sin sesión; con sesión bloquea y redirige a / |
core/states/auth/auth.{actions,effects,reducers,selectors}.spec.ts | Acciones, efectos, reducer y selectores del slice auth |
ReconnectComponent no tiene spec propio.