Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularFirebaseAuth : Calling server api just after firebase auth?

My auth is based on 2 things :

  • firebase auth (email/password)
  • call on a server API to retrieve full customer entity from BDD and from firebaseID (user must exists) So a user will be "authenticated" if these two conditions are met.

I also have authGuards based on a isAuthenticated() returning an Observable (because on a page refresh, guard must wait for the auth to be finished before redirecting the user anywhere).

Problem : I can't find a way to make that work with all the async and rxjs mess/hell .. Currently it's working but each time isAuthenticated is called, the serverAPI auth is called every time... How can I refactor that in order to call server only once and all the async/reload stuff still works ?

AuthService :

export class AuthService {
    public userRole: UserBoRole;
    public authState$: Observable<firebase.User>;

    constructor(
        private afAuth: AngularFireAuth,
        private snackBar: SnackBarService,
        private translate: TranslateService,
        private router: Router,
        private grpcService: GrpcService
    ) {
        this.authState$ = this.afAuth.authState.pipe(
            take(1),
            mergeMap(user => {
                if (!user) {
                    return of(user);
                }

                // User is successfully logged in,
                // now we need to check if he has a correct role to access our app
                // if an error occured, consider our user has not logged in, so we return null
                return this.checkProfile().pipe(
                    take(1),
                    map(() => {
                        this.test = true;
                        return user;
                    }),
                    catchError(err => {
                        console.error(err);
                        return of(null);
                    })
                );
            })
        );

        // Subscribing to auth state change. (useless here because access logic is handled by the AuthGuard)
        this.authState$.subscribe(user => {
            console.log('authState$ changed :', user ? user.toJSON() : 'not logged in');
        });
    }

    checkProfile() {
        return this.callAuthApi().pipe(
            map((customer) => {
                if (!customer || customer.hasRole() === "anonymous") {
                    return Promise.reject(new Error(AuthService.AUTH_ERROR_ROLE));
                }
                this.userRole = customer.getRole();
            })
        );
    }

    isAuthenticated(): Observable<boolean> {
        return this.authState$.pipe(map(authState => !!authState));
    }
}

AuthGuard :

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

    check(): Observable<boolean> {
        return this.authService.isAuthenticated().pipe(
            catchError(err => {
                // notifying UI of the error
                this.authService.handleAuthError(err);

                // signout user
                this.authService.signOut();

                // if an error occured, consider our user has not logged in
                return of(false);
            }),
            tap(isAuthenticated => {
                if (!isAuthenticated) {    
                    // redirecting to login
                    this.router.navigate(['login']);
                }
            })
        );
    }

    canActivateChild(): Observable<boolean> {
        return this.check();
    }

    canActivate(): Observable<boolean> {
        return this.check();
    }
}

Thanks

like image 866
Jscti Avatar asked Oct 19 '18 16:10

Jscti


2 Answers

You can change your checkProfile() function to return observable instead of observable from http request or promise in case of error. First you will check if the user already authenticated(I assumed that userRole will be fine since you save it after call to back end) and if yes return a newly created observable without call to your back end, otherwise you will make a request and emit your observable based on result of http call. With next example you will make call only once:

checkProfile() {
  return new Observable((observer) => {
    if (this.userRole) {
      observer.next();
      observer.complete();
    } else {
      this.callAuthApi().pipe(
          map((customer) => {
            if (!customer || customer.hasRole() === "anonymous") {
              observer.error(new Error(AuthService.AUTH_ERROR_ROLE));
              observer.complete();
            }
            this.userRole = customer.getRole();
            observer.next();
            observer.complete();
          })
      );
    }
  });
}
like image 105
Yevgen Avatar answered Sep 17 '22 13:09

Yevgen


Haha, ReactiveX is not easy one. It has a quite steep learning curve. But it is really powerful.

1. call server only once

You can use shareReplay.

To understand how shareReplay works, have a look here https://ng-rxjs-share-replay.stackblitz.io

//shareReplay example
ngOnInit() {    
    const tods$ = this.getTodos();
    tods$.subscribe(console.log);// 1st sub
    tods$.subscribe(console.log);// 2st sub
}

getTodos(): Observable<Todo[]> {
return this.http.get<Todo[]>(this.url)
  .pipe(
    tap(() => console.log('Request')),
    shareReplay(1) // compare with comment and uncomment
  );
}  

Output with shareReplay

Request
[Object, Object, Object]
[Object, Object, Object]

Output without shareReplay

Request
[Object, Object, Object]
Request
[Object, Object, Object]

You may use shareReplay in your auth service code.

//auth.services.ts
import { shareReplay } from 'rxjs/operators';
...

this.user$ = this.afAuth.authState.pipe(
    tap(user => {
        console.log('login user$ here', user)
    }),
    switchMap(user => {
        if (user) {
            //do something
            return this.db.object(`users/${user.uid}`).valueChanges();
        } else {
            return of(null);
        }
    }),
    shareReplay(1)  //**** this will prevent unnecessary request****
);

2. async and await toPromise()

//auth.service.ts
...
getUser() {
    return this.user$.pipe(first()).toPromise();
}

//auth.guard.ts
...
async canActivate(next: ActivatedRouteSnapshot
  , state: RouterStateSnapshot
): Promise<boolean> {

  const user = await this.auth.getUser();
  //TODO your API code or other conditional authentication here

  if (!user) {
    this.router.navigate(['/login']);
  }
  return !!user;    
}

Hope this will help you.

like image 39
John Avatar answered Sep 17 '22 13:09

John