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});
}
Pay attention to this line, taken from
verifyUserRole
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 requestYou're misusing the
.map
operator inverifyAdminUser()
.map
should only be used with synchronous functions. For example:map
should not be used with asynchronous functions. For instance: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. UseswitchMap
So basically, replace
map
withswitchMap
in lines 3 and 4 ofverifyAdminUser
. You can also simplify the last line ofverifyAdminUser
by doing the reverse: changewith
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 replacecanActivate
withcanLoad
(if I want to protect the entire module), or move thecanActivate
guard to the specific route that has thecomponent:
property withinDashboardRoutingModule
(I'm guessing at the name)See the docs