Правильный способ подписки, получения и распространения пользовательских требований Firebase для компонентов в Angular из единого сервиса - PullRequest
0 голосов
/ 02 апреля 2020

Я пытаюсь найти / создать правильный (лучший) способ получения и использования пользовательских утверждений в приложении Angular. Я добавил заявление администратора через облачную функцию. То, что я хочу иметь (и то, что я пытался сделать до этого) сейчас:

  1. Получить заявки (и зарегистрированного пользователя) в одной службе (например, auth.service)
  2. Разрешить всем другим компонентам, которым необходимо прочитать утверждения, сделать это через простой API из этой службы.
  3. Не разрешать другим компонентам подписываться на authState или что-либо еще (просто прочитайте синхронно атрибуты моего auth.service )

Зачем мне это нужно? - потому что я считаю, что он более читабелен и его легче поддерживать

(только читая (подписываясь на) authState в одном месте (например, authService.ts), что упрощает обслуживание и позволяет другим компонентам синхронно читать утверждения из 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;
      }

    }

login.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 - у меня была идея выбросить из этого су bject claimsSubject и в login.component.ts на POINT_IN_CODE_#3 подпишитесь на него и знайте, что, если оно имеет значение 1, заявки были получены в auth.service.ts из authState.

В login.component.ts в POINT_IN_CODE_#2 я знаю, что могу получить претензии от resp.getIdTokenResult, но он просто не "чувствует" правильно ... о чем этот вопрос, в основном. ..

Конкретный вопрос, который я мог бы задать, заключается в следующем:

Я хочу иметь возможность перенаправить пользователя после входа на страницу admin, если у него есть пользовательские претензии 'admin' .

Я хотел бы сделать это, как я уже говорил выше (если это возможно, И если это хорошо / улучшая читаемость / улучшая_основность), без , подписавшихся на authState напрямую, но через некоторую «вещь» из auth.service.ts.

Я бы использовал тот же «logi c», чтобы сделать, например, AuthGuard, который бы просто вызывал authService.hasClaim('admin'), и не должен был бы подписаться на authState самому, чтобы сделать проверку.

NB Я хочу знать, хорошо ли, как я это сделал, если есть какие-либо cavea тс или просто простые улучшения. Все предложения и комментарии приветствуются, поэтому, пожалуйста, оставляйте комментарии, особенно на мой Зачем мне это нужно? part!

Edit-1: Добавлена ​​подсветка машинописного кода и указано точное место в моем коде, которое не работает так, как я хочу.

Edit-2: Отредактировал некоторые комментарии относительно причин, по которым мой authService .user был нулевым ... (У меня был какой-то прогон кода, который установил его на ноль, прежде чем он был проверен в компоненте входа ...)

1 Ответ

0 голосов
/ 03 апреля 2020

ОК, так что ... я нашел способ сделать это.

Во-первых, чтобы уточнить, что именно я "чувствовал", было неправильно с идеей подписки на authState в каждом компоненте. для этого нужно было что-то узнать (будь то состояние пользователя, документ пользователя или утверждения):

Было бы очень сложно поддерживать (особенно вносить изменения) код в каждом компоненте. потому что я должен был бы обновить любую логику c относительно поиска данных. Кроме того, каждый компонент должен будет выполнять проверку данных (например, проверять, содержат ли утверждения «admin») самостоятельно, и, конечно, это может быть сделано только один раз при входе / выходе пользователя и распространено среди тех, кому требуется это.

В решении, которое я принял, это именно то, что я сделал. Все, что касается утверждений и зарегистрированного состояния пользователя, управляется authService.

Мне удалось сделать это, используя Rx JS Предметы .

. машинописный код моего сервиса, компонент входа в систему и компонент панели навигации теперь выглядит следующим образом:

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

}


login.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']);
          }
        }
      )
  }

}


nav-bar.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.

Примечание: Я отмечу это как принятый ответ, но не стесняйтесь комментировать и задавать вопросы.

...