Implementar cierre de sesión debido a inactividad del usuario

Cierre de sesión con Angular + Firebase

Si alguna vez en tu aplicación necesitas implementar la lógica de cierre de sesión automatico cuando tus usuarios han dejado de ejecutar alguna actividad en la aplicación, te comparto un ejemplo de implementación, en este caso, usando Angular y Firebase.

Para llevarlo a cabo te comparto los aspectos a cubrir:

  • El cierre de sesión se ejecuta luego de 10 minutos de inactividad (se puede cambiar en base a una variable).

  • Para detectar la actividad del usuario, en este caso porque la aplicación lo requiere, se escucha los eventos del mouse [mousemove,click], si ninguno de estos sucede dentro de 10 minutos, se da por hecho que el usuario ha dejado de interactuar con la aplicación.

Para esto se implementa la función "resetInactiveUserOnApp" la cual es declarada dentro del archivo "authentication.service.ts", el cual contiene las funciones para realizar la autenticación vía firebase como el login, logout, refreshToken, etc.

//authentication.service.ts
const minutesToMilliseconds = (minutes: number) => minutes * 60000;
const AUTO_LOGOUT_INTERVAL_WAIT = minutesToMilliseconds(10);

private pauseInactiveListener = new BehaviourSubject<boolean>(false);
private resetTimerSubscription = new Subscription();

constructor(){
    // Inicializar función al crear el componente
    this.initializeInactiveListener();
}

login(){
    //Ejecutar lógica de login y al final llamar a la función para
    // inicializar el listener de inactive user. Esto debido a que
    // al ejecutar el logout se limpia la suscripción y se 
    // debe reinicializar la función
    this.initializeInactiveListener();
}

logout(){
    // Ejecutar lógica de cierre de sesión y al final 
    // limpiar suscripciones
    this.cleanComponentSubscriptions();
}

// Abre un modal con un contador de 60 segundos que permite al usuario
// elegir si desea o no continuar con la sesión abierta, si termina el
// contador y no se ha elegido alguna opción, se cierra la sesión
// automaticamente
openLogoutModal(){
    // Pausar la escucha de eventos cuando se abre el modal para validar
    // si el usuario desea continuar con la sesión abierta
    this.pauseInactiveListener.next(true);
    const dialogRef = this.dialog.open(LogoutCountdownComponent, {
      width: '350px',
      data: {},
      panelClass: 'dialog-draggable',
      hasBackdrop: true,
      disableClose: true
    });
    // Cuando se cierre la sesión, el modal devolvera un booleano 
    // que sera true o false dependiendo de si se desea o no
    // continuar con la sesión abierta
    dialogRef.afterClosed().subscribe(keepSession => {
      if (keepSession) {
        // Refrescar la sesión y luego habilita la escucha de eventos
        this.refreshSession().pipe(take(1)).subscribe(() => {
          this.pauseInactiveListener.next(false);
        })
      } else {
        // Ejecuta el logout
        this.logout().then(() => {
          this.router.navigate(['/login']);
        });
      }
    });
}

initializeInactiveListener(){
  //Escuchar los eventos del mouse que tomamos en cuenta
  // para determinar que el usuario esta interactuando con la página
  const mouseMove$ = fromEvent(document, 'mousemove');
  const click$ = fromEvent(document, 'click');
  // Pausar la escucha de los eventos cuando el observable de pausa esta
  // habilitado, esto se ejecuta al abrir el modal de log out.
  this.resetTimerSubscription = merge(mouseMove$, click$)
    .pipe(
      filter(() => !this.pauseInactiveListener.value),
      // Usando el switchmap se cancela el observable anterior si
      // todavía no se ejecuta, en este caso luego de 10min y 
      // se completa el observable si una nueva emisión es ejecutada
      switchMap(() => {
        return timer(AUTO_LOGOUT_INTERVAL_WAIT).pipe(
          takeUntil(merge(mouseMove$, click$))
        );
      })
    ).subscribe(() => {
      // Al obtener una emisión que cumple las validaciones antes
      // añadidas, se verifica la validez de la sesión y si no es
      // válida, se valida el observable de pausa, si esta desactivado
      // abre la ventana de logout de modal
      this.verifySessionIsValid().toPromise().then(credentials => {
        if (credentials) {
          if (!this.pauseInactiveListener.value) {
            this.openLogoutModal();
          }
        }
      });
    });
}

cleanComponentSubscriptions(){
    if (this.resetTimerSubscription) {
        this.resetTimerSubscription.unsubscribe();
    }
}
// Limpiar la suscripción del timer cuando el componente se destruye
ngOnDestroy(){
   this.cleanComponentSubscriptions();
}