簡體   English   中英

從單個服務訂閱、獲取和傳播 Firebase 自定義聲明到 Angular 組件的正確方法

[英]Proper way to subscribe to and get and propagate Firebase custom claims to components in Angular from a single service

我正在嘗試尋找/創建一種在 Angular 應用程序中獲取和使用自定義聲明的正確(最佳)方法。 我通過雲功能添加了管理員自定義聲明。 我現在想要的(以及我一直嘗試做的)是:

  1. 在一項服務(例如auth.service )中獲取聲明(和登錄用戶)
  2. 允許需要讀取聲明的所有其他組件通過來自該服務的簡單 API 執行此操作
  3. 不讓其他組件訂閱 authState 或其他任何東西(只需同步讀取我的auth.service的屬性)

我為什么要這個? - 因為我相信它更具可讀性和更易於維護

(通過只在一個地方(例如authService.ts )讀取(訂閱) authState ,從而使維護更容易並允許其他組件同步讀取來自authService.ts屬性/字段的聲明)


所以,我現在正在做的代碼(這不起作用......見POINTS_IN_CODE ):

auth.service.ts

    // imports omitted for brevity...
    
    @Injectable()
    export class AuthService {
    
      user: Observable<User> = of(null);
      uid: string;
      claims: any = {};
      claimsSubject = new BehaviorSubject(0);
    
      constructor(private afAuth: AngularFireAuth,
                  private afStore: AngularFirestore,
                  private functions: AngularFireFunctions) {
    
        this.afAuth.authState
          .subscribe(
            async authUser => {
              if (authUser) { // logged in
                console.log(`Auth Service says: ${authUser.displayName} is logged in.`);
                this.uid = authUser.uid;
                this.claims = (await authUser.getIdTokenResult()).claims;
                // POINT_IN_CODE_#1
                this.claimsSubject.next(1);

                const userDocumentRef = this.afStore.doc<User>(`users/${authUser.uid}`);
                // if provider is Google (or Facebook <later> (OR any other 3rd party))
                // document doesn't exist on the first login and needs to be created
                if (authUser.providerData[0].providerId === 'google.com') {
                  userDocumentRef.get()
                  .subscribe( async snapshot => {
                    if ( ! snapshot.exists) { // if the document does not exist
                      console.log(`\nNew document being created for: ${authUser.displayName}...`); // create a user document
                      await userDocumentRef.set({name: authUser.displayName, email: authUser.email, provider: 'google.com'});
                    }
                  });
                }
                this.user = userDocumentRef.valueChanges();
              }
              else { // logged out
                console.log('Auth Service says: no User is logged in.');
              }
            }
         );
    
      }
    
      login(email, password): Promise<any> {
        return this.afAuth.auth.signInWithEmailAndPassword(email, password);
      }

      hasClaim(claim): boolean {
        return this.hasAnyClaim([claim]);
      }
    
      hasAnyClaim(paramClaims): boolean {
        for (let paramClaim of paramClaims) {
          if (this.claims[paramClaim]) {
            return true;
          }
        }
        return false;
      }
      
    }

登錄.component.ts

    // imports...
    
    @Component({
      selector: 'app-login',
      templateUrl: './login.component.html',
      styleUrls: ['./login.component.css']
    })
    export class LoginComponent implements OnInit {
    
      form: FormGroup;
      hide = true;
      errorMessage = '';
      loading = false;
    
      constructor(private fb: FormBuilder,
                  public authService: AuthService,
                  private router: Router) {}
    
      ngOnInit() {
        this.logout();
    
        this.form = this.fb.group({
          username: ['test@test.te', Validators.compose([Validators.required, Validators.email])],
          password: ['Asdqwe123', Validators.compose([Validators.required])]
        });
      }
    
      submit() {
        this.loading = true;
        this.authService.login(this.form.value.username, this.form.value.password)
          .then(resp => {
            this.loading = false;
            // POINT_IN_CODE_#2

            // what I am doing right now, and what doesn't work...
            this.authService.user
              .subscribe(
                resp => {
                  if (this.authService.hasClaim('admin')) {
                    this.router.navigate(['/admin']);
                  }
                  else {
                    this.router.navigate(['/items']);
                  }
                }
              );
    
            // POINT_IN_CODE_#3
            //this.authService.claimsSubject
            // .subscribe(
            //   num => {
            //     if (num === 1) {
            //       if (this.authService.hasClaim('admin')) {
            //         this.router.navigate(['/admin']);
            //       }
            //       else {
            //         this.router.navigate(['/items']);
            //       }
            //     }
            //   });
      }
    
      logout() {
        this.authService.logout();
      }
    }

POINTS_IN_CODE

auth.service.ts POINT_IN_CODE_#1 auth.service.ts中 - 我有想法從這個主題中發出claimsSubject並在POINT_IN_CODE_#3 login.component.ts中訂閱它並知道,如果它的值為1 ,則聲明有已在auth.service.tsauthState檢索到。

POINT_IN_CODE_#2 login.component.ts ,我知道我可以從resp.getIdTokenResult獲得聲明,但它只是“感覺”不對……這就是這個問題的主要內容……

我可以問的具體問題是:

如果用戶具有“管理員”自定義聲明,我希望能夠在用戶登錄后重定向到admin頁面。

我想這樣做,因為我上述(如果可能的話,如果它是好/改善可讀性/ improving_maintainability),無需訂閱到authState直接,而是通過從一些“東西” auth.service.ts

例如,我會使用相同的“邏輯”來制作一個AuthGuard ,它只會調用authService.hasClaim('admin') ,而不必訂閱authState本身來進行檢查。

注意我想知道我的方法是否好,是否有任何警告或只是簡單的改進。 歡迎所有建議和意見,所以請發表評論,特別是關於我為什么要這個? 部分!

編輯 1:添加了打字稿代碼突出顯示並指出了我的代碼中無法按我想要的方式工作的確切位置。

編輯 2:編輯了一些關於我的 authService.user 為空的原因的評論......(我運行了一些代碼,在登錄組件檢查之前將其設置為空......)

好的,所以......我找到了一種方法。

首先,為了澄清我在每個需要從中了解某些信息的組件(無論是用戶的登錄狀態、用戶文檔還是聲明)中訂閱authState的想法到底“感覺”是錯誤的:

維護(尤其是更改)每個組件中的代碼將非常困難,因為我必須更新有關數據檢索的任何邏輯。 此外,每個組件都必須自己檢查數據(例如檢查聲明是否包含“admin”),並且肯定只能在用戶登錄/注銷時完成一次並傳播給需要的人它。

在我制作的解決方案中,這正是我所做的。 有關聲明和用戶登錄狀態的所有內容都由authService管理。

我通過使用RxJS Subjects設法做到了這一點。

我的服務、登錄組件和導航欄組件的打字稿代碼現在如下所示:

auth.service.ts

// imports...

@Injectable()
export class AuthService {

  uid: string = null;
  user: User = null;
  claims: any = {};
  isAdmin = false;

  isLoggedInSubject = new Subject<boolean>();
  userSubject = new Subject();
  claimsSubject = new Subject();
  isAdminSubject = new Subject<boolean>();

  constructor(private afAuth: AngularFireAuth,
              private afStore: AngularFirestore,
              private router: Router,
              private functions: AngularFireFunctions) {

    // the only subsription to authState
    this.afAuth.authState
      .subscribe(
        authUser => {

          if (authUser) { // logged in
            this.isLoggedInSubject.next(true);
            this.uid = authUser.uid;

            this.claims = authUser.getIdTokenResult()
              .then( idTokenResult => {
                this.claims = idTokenResult.claims;
                this.isAdmin = this.hasClaim('admin');
                this.isAdminSubject.next(this.isAdmin);
                this.claimsSubject.next(this.claims);
              });

            this.afStore.doc<User>(`users/${authUser.uid}`).get()
              .subscribe( (snapshot: DocumentSnapshot<User>)  => {
                this.user = snapshot.data();
                this.userSubject.next(this.user);
              });
          }
          else { // logged out
            console.log('Auth Service says: no User is logged in.');
          }
        }
     );
  }

  login(email, password): Promise<any> {
    return this.afAuth.auth.signInWithEmailAndPassword(email, password);
  }

  logout() {
    this.resetState();
    this.afAuth.auth.signOut();
    console.log('User just signed out.');
  }

  hasClaim(claim): boolean {
    return !!this.claims[claim];
  }

  resetState() {
    this.uid = null;
    this.claims = {};
    this.user = null;
    this.isAdmin = false;

    this.isLoggedInSubject.next(false);
    this.isAdminSubject.next(false);
    this.claimsSubject.next(this.claims);
    this.userSubject.next(this.user);
  }

}


登錄.component.ts

// imports

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {

  providers = AuthProvider;
  form: FormGroup;

  hide = true;
  errorMessage = '';
  loading = false;

  constructor(private fb: FormBuilder,
              public authService: AuthService, // public - since we want to bind it to the HTML
              private router: Router,
              private afStore: AngularFirestore) {}

  ngOnInit() {
    this.form = this.fb.group({
      username: ['test@test.te', Validators.compose([Validators.required, Validators.email])],
      password: ['Asdqwe123', Validators.compose([Validators.required])]
    });
  }


  /**
   * Called after the user successfully logs in via Google. User is created in CloudFirestore with displayName, email etc.
   * @param user - The user received from the ngx-auth-firebase upon successful Google login.
   */
  loginWithGoogleSuccess(user) {
    console.log('\nprovidedLoginWithGoogle(user)');
    console.log(user);
    this.doClaimsNavigation();
  }

  loginWithGoogleError(err) {
    console.log('\nloginWithGoogleError');
    console.log(err);
  }

  submit() {
    this.loading = true;
    this.authService.login(this.form.value.username, this.form.value.password)
      .then(resp => {
        this.loading = false;
        this.doClaimsNavigation();
      })
      .catch(error => {
        this.loading = false;
        const errorCode = error.code;

        if (errorCode === 'auth/wrong-password') {
          this.errorMessage = 'Wrong password!';
        }
        else if (errorCode === 'auth/user-not-found') {
          this.errorMessage = 'User with given username does not exist!';
        } else {
          this.errorMessage = `Error: ${errorCode}.`;
        }

        this.form.reset({username: this.form.value.username, password: ''});
      });

  }


  /**
   * Subscribes to claimsSubject (BehaviorSubject) of authService and routes the app based on the current user's claims.
   *
   *
   * Ensures that the routing only happens AFTER the claims have been loaded to the authService's "claim" property/field.
   */
  doClaimsNavigation() {
    console.log('\nWaiting for claims navigation...')
    this.authService.isAdminSubject
      .pipe(take(1)) // completes the observable after 1 take ==> to not run this after user logs out... because the subject will be updated again
      .subscribe(
        isAdmin => {
          if (isAdmin) {
            this.router.navigate(['/admin']);
          }
          else {
            this.router.navigate(['/items']);
          }
        }
      )
  }

}


導航欄.component.ts

    // imports

    @Component({
      selector: 'app-nav-bar',
      templateUrl: './nav-bar.component.html',
      styleUrls: ['./nav-bar.component.css']
    })
    export class NavBarComponent implements OnInit {

      navColor = 'primary';
      isLoggedIn = false;
      userSubscription = null;
      isAdmin = false;
      user = null;

      loginClicked = false;
      logoutClicked = false;

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

        this.authService.isLoggedInSubject
          .subscribe( isLoggedIn => {
            this.isLoggedIn = isLoggedIn;
          });

        this.authService.isAdminSubject
          .subscribe( isAdmin => {
            this.isAdmin = isAdmin;
          });

        this.authService.userSubject
          .subscribe( user => {
            this.user = user;
          });
         }

      ngOnInit() {}

    }

我希望它可以幫助那些和我一樣在任何地方訂閱authState “壞感覺”的authState

注意:我會將其標記為已接受的答案,但請隨時發表評論和提問。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM