Ado Ren
Ado Ren

Reputation: 4394

How to wait for firebase auth before starting angular app

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.

Upvotes: 9

Views: 2905

Answers (2)

Ado Ren
Ado Ren

Reputation: 4394

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)
            }
        })
    })
}

Upvotes: 3

Denis
Denis

Reputation: 309

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)

Upvotes: 0

Related Questions