Angular2 canActivate () llamando a la función asíncrona

82

Estoy tratando de usar los protectores del enrutador Angular2 para restringir el acceso a algunas páginas en mi aplicación. Estoy usando Firebase Authentication. Para verificar si un usuario ha iniciado sesión con Firebase, tengo que llamar .subscribe()al FirebaseAuthobjeto con una devolución de llamada. Este es el código para el guardia:

import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AngularFireAuth } from "angularfire2/angularfire2";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Rx";

@Injectable()
export class AuthGuard implements CanActivate {

    constructor(private auth: AngularFireAuth, private router: Router) {}

    canActivate(route:ActivatedRouteSnapshot, state:RouterStateSnapshot):Observable<boolean>|boolean {
        this.auth.subscribe((auth) => {
            if (auth) {
                console.log('authenticated');
                return true;
            }
            console.log('not authenticated');
            this.router.navigateByUrl('/login');
            return false;
        });
    }
}

Cuando se navega a una página que tiene el guardia en ella, authenticatedo not authenticatedse imprime en la consola (después de una demora esperando la respuesta de firebase). Sin embargo, la navegación nunca se completa. Además, si no he iniciado sesión, se me redirige a la /loginruta. Entonces, el problema que tengo es return trueque no muestra la página solicitada al usuario. Supongo que esto se debe a que estoy usando una devolución de llamada, pero no puedo averiguar cómo hacerlo de otra manera. ¿Alguna idea?

Evan Salter
fuente
importar Observable así -> importar {Observable} desde 'rxjs / Observable';
Carlos Pliego

Respuestas:

124

canActivatenecesita devolver un Observableque complete:

@Injectable()
export class AuthGuard implements CanActivate {

    constructor(private auth: AngularFireAuth, private router: Router) {}

    canActivate(route:ActivatedRouteSnapshot, state:RouterStateSnapshot):Observable<boolean>|boolean {
        return this.auth.map((auth) => {
            if (auth) {
                console.log('authenticated');
                return true;
            }
            console.log('not authenticated');
            this.router.navigateByUrl('/login');
            return false;
        }).first(); // this might not be necessary - ensure `first` is imported if you use it
    }
}

Falta un returny utilizo en map()lugar de subscribe()porque subscribe()devuelve un Subscriptionno unObservable

Günter Zöchbauer
fuente
¿Puede mostrar cómo usar esta clase en otros componentes?
No estoy seguro de lo que quieres decir. Usas esto con rutas, no con componentes. Ver angular.io/docs/ts/latest/guide/router.html#!#guards
Günter Zöchbauer
El Observable no funciona en mi caso. No veo ninguna salida de consola. Sin embargo, si devuelvo booleanos condicionalmente (como en los documentos), la consola se registra. ¿Es this.auth un simple Observable?
cortopy
@cortopy authes un valor emitido por el observable (podría ser solo trueo false). El observable se ejecuta cuando el enrutador se suscribe. Tal vez falta algo en su configuración.
Günter Zöchbauer
1
@ günter-zöchbauer sí, gracias por eso. No me di cuenta de que me estaba suscribiendo a un suscriptor. ¡Muchas gracias por la respuesta! Funciona muy bien
cortopy
27

Puede utilizar Observablepara manejar la parte lógica asíncrona. Aquí está el código que pruebo, por ejemplo:

import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { DetailService } from './detail.service';

@Injectable()
export class DetailGuard implements CanActivate {

  constructor(
    private detailService: DetailService
  ) {}

  public canActivate(): boolean|Observable<boolean> {
    if (this.detailService.tempData) {
      return true;
    } else {
      console.log('loading...');
      return new Observable<boolean>((observer) => {
        setTimeout(() => {
          console.log('done!');
          this.detailService.tempData = [1, 2, 3];
          observer.next(true);
          observer.complete();
        }, 1000 * 5);
      });
    }
  }
}
KobeLuo
fuente
2
Esa es en realidad una buena respuesta que realmente me ayudó. Aunque tenía una pregunta similar, la respuesta aceptada no resolvió mi problema. Este lo hizo
Konstantin
De hecho, esta es la respuesta correcta !!! Una buena forma de utilizar el método canActivate llamando a una función asíncrona.
danilo
18

canActivatepuede devolver un Promiseque resuelve un booleandemasiado

paulsouche
fuente
13

Puede devolver verdadero | falso como promesa.

import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router';
import {Observable} from 'rxjs';
import {AuthService} from "../services/authorization.service";

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private router: Router, private authService:AuthService) { }

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
  return new Promise((resolve, reject) => {
  this.authService.getAccessRights().then((response) => {
    let result = <any>response;
    let url = state.url.substr(1,state.url.length);
    if(url == 'getDepartment'){
      if(result.getDepartment){
        resolve(true);
      } else {
        this.router.navigate(['login']);
        resolve(false);
      }
    }

     })
   })
  }
}
muchacho rebelde
fuente
1
Ese nuevo objeto Promise me salva: D Gracias.
canmustu
Gracias. Esta solución espera hasta que la llamada api responda y luego redirige .. perfecto.
Philip Enc
Esto parece un ejemplo del antipatrón del constructor Promise explícito ( stackoverflow.com/questions/23803743/… ). El ejemplo de código sugiere que getAccessRights () ya devuelve una Promise, por lo que trataría de devolverla directamente return this.authService.getAccessRights().then...y devolver el resultado booleano sin terminar resolve.
rob3c
6

Para ampliar la respuesta más popular. La API de autenticación para AngularFire2 ha cambiado algo. Esta es una nueva firma para lograr un AuthGuard de AngularFire2:

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { AngularFireAuth } from 'angularfire2/auth';
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';

@Injectable()
export class AuthGuardService implements CanActivate {

  constructor(
    private auth: AngularFireAuth,
    private router : Router
  ) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):Observable<boolean>|boolean {
    return this.auth.authState.map(User => {
      return (User) ? true : false;
    });
  }
}

Nota: esta es una prueba bastante ingenua. Puede iniciar sesión en la consola de la instancia de Usuario para ver si le gustaría probar con algún aspecto más detallado del usuario. Pero al menos debería ayudar a proteger las rutas contra los usuarios que no están conectados.

Rudi Strydom
fuente
5

En la versión más reciente de AngularFire, el siguiente código funciona (relacionado con la mejor respuesta). Tenga en cuenta el uso del método "tubería".

import { Injectable } from '@angular/core';
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router';
import {AngularFireAuth} from '@angular/fire/auth';
import {map} from 'rxjs/operators';
import {Observable} from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class AuthGuardService implements CanActivate {

  constructor(private afAuth: AngularFireAuth, private router: Router) {
  }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    return this.afAuth.authState.pipe(
      map(user => {
        if(user) {
          return true;
        } else {
          this.router.navigate(['/login']);
          return false;
        }
      })
    );
  }
}

Ajitesh
fuente
Tengo 1 llamada XHR más después de isLoggedIn (), y el resultado de XHR se usa en la segunda llamada XHR. ¿Cómo tener una segunda llamada ajax que aceptará el primer resultado? El ejemplo que dio es bastante fácil, por favor, hágame saber cómo usar el mapa si también tengo otro ajax.
Pratik
2

En mi caso, necesitaba manejar un comportamiento diferente dependiendo del error de estado de respuesta. Así es como me funciona con RxJS 6+:

@Injectable()
export class AuthGuard implements CanActivate {

  constructor(private auth: AngularFireAuth, private router: Router) {}

  public canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> | boolean {
    return this.auth.pipe(
      tap({
        next: val => {
          if (val) {
            console.log(val, 'authenticated');
            return of(true); // or if you want Observable replace true with of(true)
          }
          console.log(val, 'acces denied!');
          return of(false); // or if you want Observable replace true with of(true)
        },
        error: error => {
          let redirectRoute: string;
          if (error.status === 401) {
            redirectRoute = '/error/401';
            this.router.navigateByUrl(redirectRoute);
          } else if (error.status === 403) {
            redirectRoute = '/error/403';
            this.router.navigateByUrl(redirectRoute);
          }
        },
        complete: () => console.log('completed!')
      })
    );
  }
}

En algunos casos, es posible que esto no funcione, al menos en la nextparte del tapoperador . Quítelo y agregue el bien antiguo mapcomo a continuación:

  public canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> | boolean {
    return this.auth.pipe(
      map((auth) => {
        if (auth) {
          console.log('authenticated');
          return true;
        }
        console.log('not authenticated');
        this.router.navigateByUrl('/login');
        return false;
      }),
      tap({
        error: error => {
          let redirectRoute: string;
          if (error.status === 401) {
            redirectRoute = '/error/401';
            this.router.navigateByUrl(redirectRoute);
          } else if (error.status === 403) {
            redirectRoute = '/error/403';
            this.router.navigateByUrl(redirectRoute);
          }
        },
        complete: () => console.log('completed!')
      })
    );
  }
mpro
fuente
0

Para mostrar otra forma de implementación. Según la documentación , y mencionado por otras respuestas, el tipo de retorno de CanActivate también puede ser una Promesa que se resuelve en booleano.

Nota : El ejemplo que se muestra está implementado en Angular 11, pero es aplicable a las versiones de Angular 2+.

Ejemplo:

import {
  Injectable
} from '@angular/core';
import {
  ActivatedRouteSnapshot,
  CanActivate,
  CanActivateChild,
  Router,
  RouterStateSnapshot,
  UrlTree
} from '@angular/router';
import {
  Observable
} from 'rxjs/Observable';
import {
  AuthService
} from './auth.service';

@Injectable()
export class AuthGuardService implements CanActivate, CanActivateChild {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(
    route: ActivatedRouteSnapshot, state: RouterStateSnapshot
  ): Observable < boolean | UrlTree > | Promise < boolean | UrlTree > | boolean | UrlTree {
    return this.checkAuthentication();
  }

  async checkAuthentication(): Promise < boolean > {
    // Implement your authentication in authService
    const isAuthenticate: boolean = await this.authService.isAuthenticated();
    return isAuthenticate;
  }

  canActivateChild(
    childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot
  ): Observable < boolean | UrlTree > | Promise < boolean | UrlTree > | boolean | UrlTree {
    return this.canActivate(childRoute, state);
  }
}

rishabh sethi
fuente
0

usando async await ... esperas a que se resuelva la promesa

async getCurrentSemester() {
    let boolReturn: boolean = false
    let semester = await this.semesterService.getCurrentSemester().toPromise();
    try {

      if (semester['statusCode'] == 200) {
        boolReturn = true
      } else {
        this.router.navigate(["/error-page"]);
        boolReturn = false
      }
    }
    catch (error) {
      boolReturn = false
      this.router.navigate(["/error-page"]);
    }
    return boolReturn
  }

Aquí está mi auth gaurd (@angular v7.2)

async canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    let security: any = null
    if (next.data) {
      security = next.data.security
    }
    let bool1 = false;
    let bool2 = false;
    let bool3 = true;

    if (this.webService.getCookie('token') != null && this.webService.getCookie('token') != '') {
      bool1 = true
    }
    else {
      this.webService.setSession("currentUrl", state.url.split('?')[0]);
      this.webService.setSession("applicationId", state.root.queryParams['applicationId']);
      this.webService.setSession("token", state.root.queryParams['token']);
      this.router.navigate(["/initializing"]);
      bool1 = false
    }
    bool2 = this.getRolesSecurity(next)
    if (security && security.semester) {
      // ----  watch this peace of code
      bool3 = await this.getCurrentSemester()
    }

    console.log('bool3: ', bool3);

    return bool1 && bool2 && bool3
  }

la ruta es

    { path: 'userEvent', component: NpmeUserEvent, canActivate: [AuthGuard], data: {  security: { semester: true } } },
anónimo
fuente