Search code examples
angularrxjsguardangular2-observables

Angular guard based on two observables


I'm trying to do an "AdminGuard", that should be based on two things:

  1. Is the user logged in?
  2. Does the user have admin rights?

I've an AuthService that provides two Observable.

I've done the following:

@Injectable({
  providedIn: 'root'
})
export class IsAdminGuard implements CanActivate {
  constructor(private auth: AuthService, private router: Router) { }


  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean | UrlTree> {
      console.log(this.auth)

      return combineLatest([this.auth.isLoggedIn, this.auth.isAdmin]).pipe(
        take(1),
        map((authInfo) => {
          console.log(authInfo)
          if (!authInfo[0]) {
            console.error('Access denied - Unauthorized')
            return this.router.parseUrl('/auth/');
          } else if (!authInfo[1]) {
            console.error('Access denied - Admin only')
            return this.router.parseUrl('/auth/unauthorized');
          } else {
            return true;
          }
        })
      );
  }

}

the console.log(this.auth) gets called and seems to have valid values, but the second console.log never gets called and my component isn't loaded.

If I remove the guard from my route:

  {
    path: 'admin',
    component: AdminComponent,
    //canActivate: [IsAdminGuard],
  }

it works, so I'm pretty sure it's the IsAdminGuard that doesn't work.

I'm also displaying the some other things based on the same boolean values(some *ngIf="authService.IsLoggedIn | async" which are working, so I don't really understand what I messed up?

EDIT Here is how I update the different values of the IsLoggedIn/IsAdmin/IsUser, in my AuthService:

 constructor(public afAuth: AngularFireAuth, public router: Router, private afStore: AngularFirestore) {
    this.afAuth.authState.subscribe(async user => {
      console.log('handling auth')
      if (this._roleSubscription) {
        this._roleSubscription.unsubscribe();
        this._roleSubscription = undefined;
      }
      if (user) {
        this._user.next(user);
        this._isLoggedIn.next(true);
        this._roleSubscription = this.afStore.doc<Roles>(`roles/${user.uid}`).valueChanges().subscribe(role => {
          console.log('updating roles', role)
          if (role) {
            this._isAdmin.next(role.admin == true)
            this._isUser.next(role.admin == true || role.user == true);//Admin have also an heart, they are users too!
          } else {
            this._isAdmin.next(false);
            this._isUser.next(false);
          }
        });
      } else {
        this._user.next(undefined);
        this._isLoggedIn.next(false);
        this._isAdmin.next(false);
        this._isUser.next(false);
        await this.router.navigate(['/auth']);
      }
      console.log('values updated')
    })
  }

Solution

  • You have to use a ReplaySubject which emits the latest value. A subject only emits when there is an active subscription, and a BehaviorSubject always emits as it starts with an initial value

    readonly _isLoggedIn = new ReplaySubject<boolean>(1);
    readonly _isAdmin = new ReplaySubject<boolean>(1);