Angular 4 - canActivate observable, not callable

I am trying to implement canActivate

in Angular 2/4 using RxJS Observables. I have already read another SO question . With the following code, my method canActivate

only runs once when the application starts, but hello

never prints again when the observable isLoggedIn

triggers new values.

canActivate(): Observable<boolean> {
  return this.authService.isLoggedIn().map(isLoggedIn => {
    console.log('hello');
    if (!isLoggedIn) {
      this.router.navigate(['/login']);
    }
    return isLoggedIn;
  }).first();
}

      

or this version also doesn't work:

canActivate(): Observable<boolean> {
  return this.authService.isLoggedIn().map(isLoggedIn => {
    console.log('hello');
    if (isLoggedIn) {
      this.router.navigate(['/']);
    }
    return !isLoggedIn;
  });
}

      

However, it works fine with this code:

canActivate(): Observable<boolean> {
  return Observable.create(obs => {
    this.authService.isLoggedIn().map(isLoggedIn => {
      console.log('hello');
      if (isLoggedIn) {
        this.router.navigate(['/']);
      }
      return !isLoggedIn;
    }).subscribe(isLoggedIn => obs.next(isLoggedIn));
  });
}

      

What am I doing wrong in the first part of the code?

EDIT: here is the implementation isLoggedIn

@LocalStorage(AuthService.JWT_TOKEN_KEY)
private readonly token: string;
private tokenStream: Subject<string>;

public isLoggedIn(): Observable<boolean> {
  if (!this.tokenStream) {
    this.tokenStream = new BehaviorSubject(this.token);
    this.storage.observe(AuthService.JWT_TOKEN_KEY)
      .subscribe(token => this.tokenStream.next(token));
  }
  return this.tokenStream.map(token => {
    return token != null
  });
}

      

which is using ngx-webstorage

. and RxJS BehaviorSubject

.

+3


source to share


3 answers


AuthService calls with RxJs

This was one of the things I struggled with when switching from AngularJs promises to Angular Observable pattern. You can see that promises pull notifications and observables pull push notifications. So you have to rethink your AuthService to use the push pattern. I was thinking all the time about how to pull it out, even when the working observers wrote. I couldn't stop thinking in terms of pull.

The promise template was easier. When the AuthService has been created, it will either create a promise that is allowed to "not log in" or it will create an asynchronous promise that will "restore the registration state". Then you can have a named method isLoggedIn()

that will return this promise. This allowed you to easily handle delays between showing custom data and receiving custom data.

AuthService as a Push Service

Now we're switching to Observables, and the verb is " should be changed to " when " . Making this little change will help you rethink how things will work. So let's rename" isLoggedIn "to" whenLoggedIn () ", which will be an observable that emits data when the user authenticates.

class AuthService {
     private logIns: Subject = new Subject<UserData>();

     public setUser(user: UserData) {
          this.logIns.next(user);
     }

     public whenLoggedIn(): Observable<UserData> {
          return this.logIns;
     }
}

// example
AuthService.whenLoggedIn().subscribe(console.log);
AuthService.setUser(new UserData());

      

When the user went to setUser

, it issued a subscription that the new user was authenticated.

Problems with closer

The above contains several issues that need to be fixed.

  • subscription whenLoggedIn

    will listen to new users forever. The thrust flow never ends.
  • There is no concept of "current state". The previous one setUser

    is lost after clicking on subscribers.
  • It only reports when the user is authenticated. Not if there is no current user.

We can fix some of this by switching from Subject

to BehaviorSubject

.

class AuthService {
     private logIns: Subject = new BehaviorSubject<UserData>(null);

     public setUser(user: UserData) {
          this.logIns.next(user);
     }

     public whenLoggedIn(): Observable<UserData> {
          return this.logIns;
     }
}

// example
AuthService.whenLoggedIn().first().subscribe(console.log);
AuthService.setUser(new UserData());

      

This is much closer to what we want.

Changes

  • BehaviorSubject

    will always print the last value for each new subscription .
  • whenLoggedIn().first()

    was added to subscribe and auto-unsubscribe after getting the first value. If we weren't using it BehaviorSubject

    , this would block until someone called setUser

    , which may never happen.

Problems with BehaviorSubject

BehaviorSubject

doesn't work for AuthService and I'll demonstrate some sample code here.



class AuthService {
     private logIns: Subject = new BehaviorSubject<UserData>(null);

     public constructor(userSessionToken:string, tokenService: TokenService) {
          if(userSessionToken) {
              tokenService.create(userSessionToken).subscribe((user:UserData) => {
                    this.logIns.next(user);
               });
         }
     }

     public setUser(user: UserData) {
          this.logIns.next(user);
     }

     public whenLoggedIn(): Observable<UserData> {
          return this.logIns;
     }
}

      

This is how the problem appears in your code.

// example
let auth = new AuthService("my_token", tokenService);
auth.whenLoggedIn().first().subscribe(console.log);

      

The above creates a new AuthService with a token to restore the user session, but when it starts the console just prints "null".

This is due to the fact that it BehaviorSubject

is created with an initial value null

, and the user session restore operation will occur later after the HTTP call completes. The AuthService will keep emitting null

until the session is restored, but that's a problem when you want to use route activators.

ReplaySubject is better

We want to remember the current user, but we don't emit anything until we know if there is a user or not. ReplaySubject

is the answer to this problem.

You can use it here.

class AuthService {
     private logIns: Subject<UserData> = new ReplaySubject(1);

     public constructor(userSessionToken:string, tokenService: TokenService) {
          if(userSessionToken) {
              tokenService.create(userSessionToken).subscribe((user:UserData) => {
                    this.logIns.next(user);
               }, ()=> {
                    this.logIns.next(null);
                    console.error('could not restore session');
               });
         } else {
             this.logIns.next(null);
         }
     }

     public setUser(user: UserData) {
          this.logIns.next(user);
     }

     public whenLoggedIn(): Observable<UserData> {
          return this.logIns;
     }
}

// example
let auth = new AuthService("my_token", tokenService);
auth.whenLoggedIn().first().subscribe(console.log);

      

The above will not wait until the first value is selected by value whenLoggedIn

. It will get the value first

and unsubscribe.

ReplaySubject

works because it remembers the elements 1

or emits nothing. It's nothing important. When we use AuthService in canActivate

, we want to wait until the state of the user is known.

CanActivate example

This now makes it much easier to record a user defender redirecting to the login screen or allowing a reroute.

class UserGuard implements CanActivate {
      public constructor(private auth: AuthService, private router: Router) {
      }

      public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
           return this.auth.whenLoggedIn()
                      .first()
                      .do((user:UserData) => {
                          if(user === null) {
                              this.router.navigate('/login');
                          }
                      })
                      .map((user:UserData) => !!user);
      }

      

This will result in observing true or false if there is a user session. It also blocks the router from changing until this state is known (i.e. are we fetching data from the server?).

It will also redirect the router to the login screen if there is no user data.

+13


source


canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
      return this.service.callServiceMethod())
        .map(result => {
          return true;
        }).catch(err => {
          return Observable.of(false);
        });

    return Observable.of(true);
  }

      



0


source


This is how I did it

import { Injectable, Inject, Optional } from "@angular/core";
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router";
import { Observable } from "rxjs/Rx";
import { AuthService, MyLogin, FacebookLogin, GoogleLogin } from "./auth.service";


@Injectable()

export class AuthGuard implements CanActivate  {

  constructor() {
  }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {

    // return this.typeSelector(SigninComponent.loginTypeProperty);
    let isLogged: boolean = AuthService.loggedIn;
    window.alert('isLogged =' + isLogged);
    return isLogged;
  }
}

      

This is in a different situation where I have to change the password:

import { Injectable, Inject, Optional } from "@angular/core";
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router";
import { Observable } from "rxjs/Rx";
import { NewPasswordAuthService } from "./new-password.auth.service";
import { Router, ActivatedRoute } from '@angular/router';
import { IPwCookie } from './IPwCookie.interface';


@Injectable()
export class NewPasswordGuard implements CanActivate  {


  constructor(private authService: NewPasswordAuthService, private router: Router, private route: ActivatedRoute,)
  {

  }
  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {


    console.log('query params' + JSON.stringify(route.params));
    let hash:string = route.params.hash;
    let userId: number = route.params.userId;
    console.log('query params string' + JSON.stringify(route.params.hash));
    return  this.authService.activate(hash)
        .map(
          (): boolean => {
            console.log('check passed =' + NewPasswordAuthService.isAuthenticated());

            let passwordCookie:IPwCookie = {
              hash: hash,
              userId: userId
            };

            localStorage.setItem('password_hash', JSON.stringify(passwordCookie));
            return NewPasswordAuthService.isAuthenticated();
          },
          (error) => {console.log(error)}
        );
  }

}

      

As far as I can see you should try adding a return type to the map function, this is one of the issues that gave me problems. If you don't add the return type to the map function, it doesn't work in this context.

So just do .map ((): boolean => {} ...)

0


source







All Articles