How to call an async method in canActivate with an

2019-09-14 19:58发布

问题:

I am trying to implement a canActivate guard for an admin route in Angular 4.2.4.

Based off this stack question here: canActivate Question, I think I'm doing everything correctly. But alas, I can't seem to get things to work.

Here is the module guard:

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private _store: Store<AppState>,
              private router: Router,
              private czauth: CzauthService) { }

  canActivate(route: ActivatedRouteSnapshot):Observable<boolean>{
    return this.czauth.verifyAdminUser().first();
  }
};

Here is my czauth service:

  getToken():Observable<firebase.User>{
    return this.af.idToken;
  }

  verifyUserRole(token){
      this.token = token;
      console.log(token);// Log out "PromiseObservable {_isScalar: false, promise: D, scheduler: undefined}"
      let headers = new Headers({ 'Authorization': 'Bearer ' + token});
      let options = new RequestOptions({ headers: headers });
      return this.http.get(environment.api+'/verifyadminuser', options).map((response)=>{return response.json()});
  }


  verifyAdminUser():Observable<boolean>{
    return this.getToken()
      .map((user)=>{return Observable.fromPromise(user.getIdToken())})
      .map((token)=>{return this.verifyUserRole(token)})
      .do((response)=>{console.log(response)})// Always returns "Observable {_isScalar: false, source: Observable, operator: MapOperator}"
      .switchMap((response:any)=>{ return response.status === 200 ? Observable.of(true) : Observable.of(false)});
  }

I can never get the response from my http async call. I always get what looks to be a cold observable in the console. It's as if the router never subscribes to my observable? I would like to return a boolean based off what my http response is. How can I achieve this?

EDIT:

I am calling the verifyAdminUser method from my guard. The service is a sinlgeton on the root module. The guard is protecting access to a lazy-loaded module.

Below I have included where I am using the guard in the routing.

Routing Module:

const routes: Routes = [
  {path: '', loadChildren : './landing/landing.module#LandingModule'},
  {path: 'dashboard', loadChildren : './dashboard/dashboard.module#DashboardModule', canActivate: [ AuthGuard ]}
];

@NgModule({
  imports: [RouterModule.forRoot(routes, {preloadingStrategy: PreloadAllModules})],
  exports: [RouterModule]
})
export class AppRoutingModule { }

The trouble is when the user tries to navigate to the dashboard module, canActivate always returns false because the response in my switchMap operator is undefined.

EDIT 2:

I refactored and simplified the following two methods in my code, and now everything works just fine. Now, I'm trying to understand why. Here are the adjusted to methods:

  verifyUserRole(){
      let headers = new Headers({ 'Authorization': 'Bearer ' + this.token});
      let options = new RequestOptions({ headers: headers });
      return this.http.get(environment.api+'/verifyadminuser', options).map((response)=>{return response});
  }


  verifyAdminUser():Observable<boolean>{
    return this.getToken()
      .map((user)=>{
        user.getIdToken().then((token)=>this.token = token);
        return user;
      })
      .map(()=>{return this.verifyUserRole})
      .switchMap((response:any)=>{ return response ? Observable.of(true) : Observable.of(false)});
  }

Solution:

Thanks to @BeetleJuice excellent answer below, I was able to create a derivative solution. Here are the two methods I refactored:

  verifyUserRole(token){
    this.token = token;
    let headers = new Headers({ 'Authorization': 'Bearer ' + token});
    let options = new RequestOptions({ headers: headers });
    return this.http.get(environment.api+'/verifyadminuser', options).map((response)=>{return response});
  }


  verifyAdminUser():Observable<boolean>{
    return this.getToken()
      .switchMap((user)=>{return Observable.fromPromise(user.getIdToken())})
      .switchMap((token)=>{return this.verifyUserRole(token)})
      .map((response:any)=>{ return response.status === 200 ? true : false});
  }

回答1:

Pay attention to this line, taken from verifyUserRole

console.log(token);// Log out "PromiseObservable...

So token is an observable, but you're treating it as a string on the very next line so the server will probably reject that request

let headers = new Headers({ 'Authorization': 'Bearer ' + token})

You're misusing the .map operator in verifyAdminUser(). map should only be used with synchronous functions. For example:

// this works if user.id is a property available immediately
return this.getToken().map(user => user.id)

map should not be used with asynchronous functions. For instance:

// this fails since the return value is an Observable that will resolve later
return this.getToken().map(user => Observable.fromPromise(...))

map returns immediately. As a result, what gets passed down the chain is not the token itself as you expected, but rather an Observable that will produce the token in the future. That's whyconsole.log(token) gave you "PromiseObservable". What you need is an operator that will wait for the observable in it to produce, then pass the emitted value to the next operator. Use switchMap

//this will work; switchMap waits for Obervable.fromPromise 
// to emit before continuing
return this.getToken().switchMap(user => Observable.fromPromise(...))

So basically, replace map with switchMap in lines 3 and 4 of verifyAdminUser. You can also simplify the last line of verifyAdminUser by doing the reverse: change

.switchMap((response:any)=>{ return response.status === 200 ? Observable.of(true) : Observable.of(false)})

with

// no need for switchMap because response.status is available immediately
.map(res => res.status===200? true: false)

Also, you are using canActivate incorrectly. This is meant to guard against the activation of a component. To guard against the loading of a lazy-loaded module, use canLoad guard. So I would either replace canActivate with canLoad (if I want to protect the entire module), or move the canActivate guard to the specific route that has the component: property within DashboardRoutingModule (I'm guessing at the name)

See the docs