How to wait for firebase auth before starting angular app
Asked Answered
N

2

9

I would like to display a small loading logo while the firebase authentication is retrieving a user token, before starting "for real" the single page application.

So far I have an authentication service :

constructor(
    public afAuth: AngularFireAuth,
    ) {
      this.afAuth.onAuthStateChanged(user => {
        if (user) {
           this.setCredentials(user)
        }
      })
    }

  setCredentials(user: firebase.User) {
      return user.getIdTokenResult(true).then(idTokenResult => {
        this.credentials = {
          userId: idTokenResult.claims.id,
          role: idTokenResult.claims.role,
          token: idTokenResult.token,
        };
        // STARTS THE APPLICATION NOW ?
      })
  }

Is it possible to achieve such behavior ? I've read about APP_INITIALIZER without success. I want to avoid localstorage / session storage and instead rely solely on this initialization.

Update :

created an init function :

export function initApp(auth: AuthService, afAuth: AngularFireAuth) {
    return () => {
      return new Promise((resolve) => {
        afAuth.user.pipe(
            take(1),
        ).subscribe(user => {
          if (user) {
            auth.setCredentials(user)
            .then(() => resolve())
          } else {
              resolve();
          }
        })
      });
    }
  }

And edited AppModule providers:

providers: [
    interceptorProviders /* my interceptors */,
    {
      provide: APP_INITIALIZER,
      useFactory: initApp,
      deps: [AuthService, AngularFireAuth],
      multi: true
    }
  ]

Still need to figure out how to add a waiting logo but it's another question. I'll update asap.

Necessitate answered 30/5, 2020 at 8:23 Comment(0)
N
3

Answering to my own question

To summarize I wanted to make sure my token claims (role, and user id per say) associated with a firebase user were stored in my auth service before dealing with routing, because components inside these routes would use those credentials.

In the end I did not follow the APP_INITIALIZER that is not really a good solution.

Auth Service

private _credentials: BehaviorSubject<Credentials> = new BehaviorSubject<Credentials>(null);
public readonly credentials$: Observable<Credentials> = this._credentials.asObservable();

constructor(private afAuth: AngularFireAuth) {
this.afAuth.authState.subscribe(user => {
      this._credentials.next(null);
      if (user) {
        user.getIdTokenResult().then(data => {
          const credentials = {
            role: data.claims.role,
            token: data.token,
            userId: data.claims.userId
          }

          this._credentials.next(credentials);
          console.log(credentials);
        })
      } else {
        this._credentials.next({role: null, token: null, userId: null});
      }
    })
}

get credentials(): Credentials {
    return this._credentials.value;
}

Display a waiting spinner in app.component

Below prevents routes from displaying if credentials not set. In the template :

<div *ngIf="!(credentials$ | async)" class="logged-wrapper">
    <div class="spinner-wrapper">
        <mat-spinner class="spinner"></mat-spinner>
    </div>
</div>
<router-outlet *ngIf="(credentials$ | async)"></router-outlet>

In the component :

credentials$: Observable<any>;

constructor(
    private auth: AuthService,
  ) {
    this.credentials$ = this.auth.credentials$;
  }

Auth Guard

The takewhile allows me to make sure my credentials are set before going further.

canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot):Promise<boolean> {
    return new Promise((resolve) => {
        this.auth.credentials$.pipe(
            takeWhile(credentials => credentials === null),
        ).subscribe({
            complete: () => {
                const credentials = this.auth.credentials
                if (!credentials.role) {
                    this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } })
                    resolve(false);
                }
                if (next.data.roles && next.data.roles.indexOf(credentials.role) === -1) {
                    this.router.navigate(['/']);
                    resolve(false);
                }
                resolve(true)
            }
        })
    })
}
Necessitate answered 18/8, 2020 at 12:57 Comment(2)
Doing this on the "wrong" route will throw Firebase: No Firebase App '[DEFAULT]' has been created. It appears that the guard gets called before AngularFireModule.initializeApp(config) has finished. I have absolutely no idea how this is even possible but it's what I see..Stove
About the spinner: you don't have to make two *ngIfs: You can just make one, and add else loading while adding #loading to your loading-div or ng-template. Furthermore, it's nice to add e.g. let creds to keep your async credentials$ in a new variable for the html - e.g. in later *ngFor: <div *ngIf="(credentials$ | async); else loading; let creds"> ... (show things - maybe with *ngFor="let cred of creds") ... </div> <div #loading>Wait while I am loading...</div>Deft
S
0

You should use your authentication service in a CanActivate router guard: https://angular.io/api/router/CanActivate

This means your AppModule will initially load and then your child route (ex. MainModule with router path '') has the guard. Then in AppModule you can check for the status of the service and show a loading information until MainModule is activated (when firebase auth is finished)

Sellma answered 30/5, 2020 at 8:49 Comment(4)
Would it not load everytime I go to a new route ?Necessitate
depends on what you need to accomplish, actually, you can also check in the component if the service is still loading and just switch the design with *ngIf but if you want to prevent a component to load you would need to use a guard. you can save the result in a local variable and return true if its set (then you can prevent re-catching the user from firebase). a good starting point is also found here. fireship.io/lessons/angularfire-google-oauthSellma
Had a rough time figuring out your solution. Went for another one using APP_INITIALIZER that works so far. Do you think going for your solution would be more efficient ? Updated my question with code I've implemented.Necessitate
My solution would make angular aware of the status of the fire base authentication. So I guess my way would be more something that goes the angular way of routing/authenticating things, while yours tries to stop angular from doing anything before authentication, which might work (can not tear) but might induce problems later on.Sellma

© 2022 - 2024 — McMap. All rights reserved.