user10181542
user10181542

Reputation: 1370

Angular Firestore Admin Auth Guard

I am trying to prevent regular users from accessing the admin side of my app. Currently, the admin property on the Firestore document is either true or false. If false, they should be redirected to the home page when trying to access it, otherwise allow the user to continue to the page.

Here is my admin-auth-guard.service.ts

  userDoc: AngularFirestoreDocument<User>;
  user: Observable<User>;

  constructor(private afAuth: AngularFireAuth,
              private afs: AngularFirestore,
              private router: Router) {}

  canActivate() {
    this.userDoc = this.afs.doc('users/' + this.afAuth.auth.currentUser.uid);
    this.user = this.userDoc.valueChanges();
    return this.user.map(role => {
      if (role.admin)
          return true
        this.router.navigate(['/']);
          return false;        
    });
  }

I believe the code will work, though when initializing the Admin Auth Guard, the request happens too fast and uid is null.

I receive this error:

ERROR Error: Uncaught (in promise): TypeError: Cannot read property 'uid' of null

TypeError: Cannot read property 'uid' of null

Firestore and angular seems to be a niche topic, not much information on both together. Any help would be appreciated!

Upvotes: 1

Views: 1774

Answers (1)

DarkLeafyGreen
DarkLeafyGreen

Reputation: 70416

You can not rely on uid being initialized when you access it synchronously, because it will be populated by firebase when the user is (internally) authenticated. I would rather treat it as an observable and access it asynchronously.

So lets make it an observable, create a auth.service.ts file:

Injectable()
export class FirebaseAuthService {

  constructor(
    private angularFireAuth: AngularFireAuth,
    private angularFirestore: AngularFirestore,
  ) {
  }

  getUser(): Observable<any> {
    return this.angularFireAuth.authState.pipe(
      mergeMap(authState => {
        if (authState) {
          return from(this.angularFirestore.doc(`users/${authState.uid}`).get());
        } else {
          return NEVER;
        }
      })
    );
  }

  // is logged in?

  // signin

  // more auth related methods
}

So one can subscribe to getUser(). Whenever the global auth state changes, authState will emit an item. When this item is defined (thus the user is authenticated) a request will be made to firestore, to retrieve the user document. When the authState is not defined (thus the user is not authenticated) NEVER will be emitted, which means that nothing will be returned and the observable will not be terminated.

Now use this class in your auth guard:

@Injectable()
export class AdminAuthGuardService implements CanActivate {

  constructor(
    private router: Router,
    private firebaseAuthService: FirebaseAuthService,
  ) {
  }

  canActivate() {
    return this.firebaseAuthService.getUser().pipe(
      map(user => {
        if (!user || !user.admin) {
          // noinspection JSIgnoredPromiseFromCall
          this.router.navigate(['/login']);
          return false;
        }
        return true;
      }),
      take(1),
    );
  }
}

We keep the complexity inside the auth service and as a consequence the auth guard gets quite dumb. It subscribes to getUser and redirects to login when the user is undefined (not authenticated) or not an admin.

Upvotes: 2

Related Questions