6

I'm new to rxjs and would like some help on how to solve this.

I want to pass an Observer to onAuthStateChanged(), which takes an observer object. The observer would do some work and emit a boolean value such that the boolean value can be returned as an Observable. How do I go about implement this bridge of from observable to observer?

export class AuthGuard implements CanActivate {

constructor(private firebase: FirebaseService, private router: Router) {
}

canActivate(): Observable<boolean> {
    this.firebase.auth.onAuthStateChanged(/* an observer */)
    return /* an Observable<boolean> */
    }
}
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
Sam
  • 1,288
  • 1
  • 13
  • 22
  • You could the function overload of `onAuthStateChanged`, use a `replaySubject` to memorize the last value emitted by the subject, use that subject in your function to emit some boolean. Then return that same subject. A subject is both an observable and an observer. So the subject would receive the value from the function. Being also an observable, other listeners can subscribe to it, and get that value back. Because you use a `replaySubject`, even if your listeners would subscribe to the subject after it emitted, they would still be able to get the value back. – user3743222 Aug 24 '16 at 00:56

5 Answers5

6

Since onAuthStateChanged takes an observer as input, and returns the teardown function, we can simply wrap it with:

Rx.Observable.create(obs => firebase.auth().onAuthStateChanged(obs))

Actually for strange reasons this might not work, and we can do:

var onAuthStateChanged$ = Rx.Observable.create(obs => {
  return firebase.auth().onAuthStateChanged(
    user => obs.next(user),
    err => obs.error(err),
    () => obs.complete());
})

Now if you are unfamiliar with the Observable.create function, let me explain: create takes a onSubscribe function that hands in an observer and returns the teardown function. Doesnt that sounds very familiar with now onAuthStateChanged is build up? You hand in nextOrObserver and it returns the teardown!

(Now for strange reasons nextOrObserver did not accept an observer for me, so i switched to giving it a next function instead. Hench the code above.)

With the onAuthStateChanged$ set up, we can transform the stream using operators. All operators do is transform one observable into another, and RxJs has several dozen of these. In your case, it might look like this:

canActivate(): Observable<boolean> {
  onAuthStateChanged$
    .do(user => {if (!user) { this.router.navigate(['/login']); } })
    .map(user => !!user)
    .do(user => console.log('Authenticated?', user))
}
Dorus
  • 7,276
  • 1
  • 30
  • 36
4

To benefit others, here's what I ended up writing and it seems to work well.

import 'rxjs/add/operator/do';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/take';

import { Observable } from 'rxjs/Observable';
import { ReplaySubject } from 'rxjs/ReplaySubject';
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { FirebaseService } from '../shared/firebase.service';


@Injectable()
export class AuthGuard implements CanActivate {

    loggedInSubject: ReplaySubject<any>;

    constructor(private firebase: FirebaseService, private router: Router) {
        this.loggedInSubject = new ReplaySubject(1);
        this.firebase.auth.onAuthStateChanged(this.loggedInSubject);
    }

    canActivate(): Observable<boolean> {
        return this.loggedInSubject.map(user => {
            if (!user) {
                this.router.navigate(['/login']);
            }
            console.log('Authenticated?', !!user);
            return !!user;
        }).take(1);
    }

}
Sam
  • 1,288
  • 1
  • 13
  • 22
  • "onAuthStateChanged failed: First argument "nextOrObserver" must be a valid object or a function." – SeanMC Aug 18 '18 at 00:46
2

Here's the short version, a helper function you can place anywhere...

export function MakeAuthstateObservable(
  auth: firebase.auth.Auth
): Observable<firebase.User> {
  const authState = Observable.create((observer: Observer<firebase.User>) => {
    auth.onAuthStateChanged(
      (user?: firebase.User) => observer.next(user),
      (error: firebase.auth.Error) => observer.error(error),
      () => observer.complete()
    );
  });
  return authState;
}

Ben Winding
  • 10,208
  • 4
  • 80
  • 67
1

Not sure if this is necessarily 'better' than the answers above, but it's certainly cleaner. I decided to create two properties on the AuthService, one as just a boolean to reflect whether the user is authenticated, and a userLoggedIn subject which basically emits the value of the boolean property. Both properties are bound with onAuthStateChanged(). So once the state changes, the authenticated property becomes true, if authenticated, otherwise false, and userLoggedIn emits this value using next() (next(this.authenticated)). On the AuthGuard I set CanActivate() to return a boolean or Observable<boolean>. First, if the authenticated property on the AuthService is checked, and if it is returns true, otherwise it maps the userLoggedIn subject to find out whether or not the user has been authenticated. This means that after the page refreshes the guard will return the value of the emitted subject because authenticated is not yet defined, so instead just waits for userLoggedIn to return. The reason to have a check for the authenticated property first is that if you tried to change page using a nav link nothing would happen because the guard only returns the emitted value of the subject, which is only called when the state of authorisation changes - i.e. login, logout, or page-refresh (re-bootstrapping application). Code below:

AuthService

import * as firebase from 'firebase';
import { Router } from '@angular/router';
import { Injectable, OnInit } from '@angular/core';
import { Subject } from 'rxjs/Subject';

@Injectable()

export class AuthService implements OnInit {
    authenticated: boolean;
    userLoggedIn = new Subject<boolean>();

    constructor(private router: Router) {}

    ngOnInit() {
    }

    checkAuthStatus() {
        firebase.auth().onAuthStateChanged((user) => {
            this.authenticated = !!user;
            this.userLoggedIn.next(this.authenticated);
        });
    }

    login(email: string, password: string) {
        firebase.auth().signInWithEmailAndPassword(email, password).then(() => {
            this.authenticated = true;
            this.router.navigate(['/']);
        }).catch((error) => {
            console.log(error);
        });
    }

    logout() {
        firebase.auth().signOut().then(function() {
            this.router.navigate(['login']);
        }.bind(this)).catch((error) => {
            console.log(error);
        });
    }
}

AuthGuard

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

import 'rxjs/add/operator/map';

@Injectable()

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

    canActivate(): Observable<boolean> | boolean {
        if(this.authService.authenticated) {
            return true;
        }

        return this.authService.userLoggedIn.map((authenticated) => {
            if(!authenticated) {
                this.router.navigate(['login']);
            }

            return authenticated;
        });
    }
}
A. Appleby
  • 469
  • 1
  • 10
  • 23
0

Similar approach:

./auth-guard.ts

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

import { AuthService } from '../shared/auth.service';

@Injectable()
export class AuthGuard implements CanActivate {

  constructor(
    private router: Router,
    private authService: AuthService) { }

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

    return this.authService.authState.map((auth) => {
      if (auth == null) {
        this.router.navigate(['auth']);
        return false;
      } else {
        return true;
      }
    }).first();

  }
}

./shared/auth.service.ts

import { Observable } from 'rxjs/Observable';
import { Observer } from 'rxjs/Observer';
import { FirebaseApp } from '../shared/firebase';

@Injectable()
export class AuthService {

  public auth: firebase.auth.Auth;
  public authState: Observable<firebase.User>;

  constructor(public app: FirebaseApp) {
    this.auth = app.auth();
    this.authState = this.authStateObservable(app);
  }

  /**
   * @function
   * @desc Create an Observable of Firebase authentication state
   */
  public authStateObservable(app: FirebaseApp): Observable<firebase.User> {
    const authState = Observable.create((observer: Observer<firebase.User>) => {
      this.auth.onAuthStateChanged(
        (user?: firebase.User) => observer.next(user),
        (error: firebase.auth.Error) => observer.error(error),
        () => observer.complete()
      );
    });
    return authState;
  }
}

./shared/firebase.ts

import * as firebase from 'firebase';

export class FirebaseApp implements firebase.app.App {

  name: string;
  options: {};
  auth: () => firebase.auth.Auth;
  database: () => firebase.database.Database;
  messaging: () => firebase.messaging.Messaging;
  storage: () => firebase.storage.Storage;
  delete: () => firebase.Promise<any>;

  constructor() {
    return firebase.initializeApp({
      apiKey: 'AIzaSyC6pDjAGuqXtVsU15erxVT99IdB0t4nln4',
      authDomain: 'inobrax-ebs-16552.firebaseapp.com',
      databaseURL: 'https://inobrax-ebs-16552.firebaseio.com',
      storageBucket: 'inobrax-ebs-16552.appspot.com',
      messagingSenderId: '383622803653'
    });
  }

}
gcfabri
  • 564
  • 2
  • 7
  • 28