Удостоверяване на Firebase: Вход

AngularJS към Angular 7 и още... моето лично пътуване ❤️☕️

Добре дошли обратно 👋

Това е продължението на много истории, започнали от този първи среден пост.



Днешната история ще обхване създаването на компонента за влизане и свързването му за удостоверяване с Firebase.

Предпоставки

Преди да започнем, уверете се, че сте следвали предишните две истории:





🐘 Искам да използвам PostgreSQL вместо Firestore

Вижте другите ми истории, които преработват логиката за използване на PostgreSQL





🚀 Да започваме!

Нуждаем се от нов компонент за влизане, но тъй като зареждаме мързеливо, искаме да пропуснем импортирането в NgModule. Също така не искаме да създаваме тестови файлове. Ето как ще изглежда нашата команда:

ng g c pages/authentication/login --skip-import --skip-tests

Имаме общ дизайн, който трябва да бъде включен. Отворете файла login.component.tsи добавете authentication.component.cssкъм stylesUrls:

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: [
    './login.component.css',
    '../authentication.component.css'
  ]
})

Дизайн на страница / форма

Ето едно напомняне за това как искаме да изглежда нашата страница и някои от класовете, които ще спомена, са от историята, Удостоверяване на Firebase: Настройка и споделен дизайн:

Обвивка на страница

Отворете login.component.html и нека започнем със създаването на div елемент с класа form-page-wrapper

<div class="form-page-wrapper" fxLayout="row" fxLayoutAlign="start">
</div>

Въведение в страницата

Вътре в този div можем да добавим нашата секция „Въведение в страницата“ с изображение и заглавие на логото на компанията.

<section class="form-page-intro" fxFlex fxHide fxShow.gt-xs>
  <div class="logo">
    <img src="assets/images/logos/company.logo.png">
  </div>
  <div class="title company">
    <span><b>Company</b> Name</span>
  </div>
</section>

Искаме тази секция да се преоразмерява въз основа на прозореца, така че добавяме fxFlex Също така, нека се скрием на малки екрани и да покажем, когато над 600px със следното fxHide fxShow.gt-xs

форма за влизане

Следващият раздел е нашите form-wrapper и form-section

<div class="form-wrapper">
  <section class="form-section"></section>
</div>

Първият елемент, който ще добавим към form-section, е друго лого на компанията, но искаме това да се показва само на мобилни устройства fxHide.gt-xs

<section ...>
  <div class="logo" fxHide.gt-xs>
    <img src="assets/images/logos/company.logo.png">
  </div>
</section>

След това имаме нужда от заглавие, така че потребителят да знае къде се намира

<div class="title">LOGIN TO YOUR ACCOUNT</div>

След това имаме <form>, където потребителят може да въведе своя имейл / парола, да отиде до „Забравена парола?“ страница, отворете страницата за регистрация „Създаване на акаунт“ или влезте с Google.

Ще използваме FormBuilderза да конфигурираме формуляра си и да обработваме проверката, така че нека импортираме класовете, от които се нуждаем, във файла login.component.ts

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
...

Вътре в класа LoginComponent трябва да създадем свойството за нашата FormGroup и различните проверки:

...
export class LoginComponent implements OnInit {
  loginForm: FormGroup;
loginValidation = {
    email: [
      { type: 'required', message: 'Email is required' },
      { 
        type: 'email', 
        message: 'Please enter a valid email address' 
      }
    ],
    password: [
      { type: 'required', message: 'Password is required' },
      { 
        type: 'minlength', 
        message: 'Password must be at least 5 characters long' 
      }
    ]
  };
constructor(
    private formBuilder: FormBuilder
  ) { }
ngOnInit(): void {
    this.loginForm = this.formBuilder.group({
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, Validators.minLength(5)]]
    });
  }
}

Нека обвържем свойството formGroup към loginFormна нашия елемент на формуляр и да зададем novalidateтъй като не искаме да проверяваме при изпращане

<form [formGroup]="loginForm" novalidate>

Следващият елемент ще бъде нашето mat-form-fieldза потребителя да попълни имейла. HTML ще изглежда така:

<mat-form-field appearance="outline">
  <mat-label>Email</mat-label>
  <input matInput type="email" 
                  formControlName="email" 
                  autocomplete="email" required>
  <mat-icon matSuffix>mail</mat-icon>
  <mat-error *ngFor="let validation of loginValidation.email">
    <mat-error   *ngIf="loginForm.get('email').hasError(validation.type) && 
      (loginForm.get('email').dirty || 
      loginForm.get('email').touched)">
      {{validation.message}}
    </mat-error>
  </mat-error>
</mat-form-field>

Можете да намерите подробности за този компонент в документацията на Angular Material тук. Свързахме входа към formBuilderсъс следното formControlName="email"

Обратно в нашия файл login.component.tsвътре в метода ngOnInitние конфигурирахме полетата за имейл/парола, зададохме първоначалната стойност на празна и добавихме някои Валидатор типове.

За да показваме съобщения за всеки от тези типове, създадохме свойство loginValidation за всяка от контролите и имаме масив от типове/съобщения, които ще се показват, когато някое от условията е изпълнено:

loginValidation = {
  email: [
    { type: 'required', message: 'Email is required' },
    { 
      type: 'email', 
      message: 'Please enter a valid email address' 
    }
  ],
  password: [
    { type: 'required', message: 'Password is required' },
    { 
      type: 'minlength', 
      message: 'Password must be at least 5 characters long' 
    }
  ]
};

Преминаваме през масива в loginValidation.email и определяме дали нашият loginFormвъведен имейл има грешка, която съответства на дефинирания тип валидатор и е мръсен или докоснат

<mat-error *ngFor="let validation of loginValidation.email">
  <mat-error 
    *ngIf="loginForm.get('email').hasError(validation.type) && 
          (loginForm.get('email').dirty || 
          loginForm.get('email').touched)">
    {{validation.message}}
  </mat-error>
</mat-error>

❓ Защо да проверявате мръсни и пипани

Може да не искате вашето приложение да показва грешки, преди потребителят да има възможност да редактира формуляра. Проверките за мръсни и докоснати предотвратяват показването на грешки, докато потребителят не направи едно от следните две неща: промени стойността, завъртайки контрола мръсен; или замъглява контролния елемент на формуляра, настройвайки контролата на докоснат.

Можем да приложим това към въвеждането на нашата парола, като посочим loginValidation.password. След това cпроменете loginForm.get от имейлнапарола.

<mat-form-field appearance="outline">
  <mat-label>Password</mat-label>
  <input matInput type="password" 
              formControlName="password" 
              minlength="5" 
              autocomplete="current-password" 
              required>
  <mat-icon matSuffix>vpn_key</mat-icon>
  <mat-error *ngFor="let validation of loginValidation.password">
    <mat-error *ngIf="loginForm.get('password').hasError(validation.type) && 
              (loginForm.get('password').dirty || 
              loginForm.get('password').touched)">
      {{validation.message}}</mat-error>
  </mat-error>
</mat-form-field>

Запомни ме & Забравена парола?

Този HTML/CSS е сравнително ясен и не искам да ви отегчавам с подробности. Обърнете се към API за повече информация относно fxLayout и fxLayoutAlign. Ако имате конкретни въпроси, не се колебайте да ги зададете.

CSS

.form-section>form mat-checkbox {
  margin: 0;
}
.form-section>form>.remember-forgot-password {
  margin-top: 8px;
}
.form-section>form>.remember-forgot-password>.remember-me {
  margin-bottom: 16px;
}
.form-section>form>.remember-forgot-password>.forgot-password {
  margin-bottom: 16px;
  color: #2196F3;
  font-weight: 700;
}

HTML

<div class="remember-forgot-password" 
     fxLayout="row" 
     fxLayout.lt-lg="column" 
     fxLayoutAlign="space-between center">
  <mat-checkbox class="remember-me" 
                aria-label="Remember Me">
    Remember Me
  </mat-checkbox>
<a class="forgot-password" 
     [routerLink]="'/auth/forgot-password'">
    Forgot Password?
  </a>
</div>

Запомни ме няма никаква функционалност на този етап и за момента е там само като контейнер.

Влизам

Стилът за нашия бутон вече е дефиниран в предишна история, но искаме нашият бутон да остане деактивиран, докато нашият loginForm има валидни записи. Правим това с този ред код [disabled]="loginForm.invalid" Също така, нека свържем събитие за щракване към функция, onLoginWithEmail().

<button mat-raised-button 
        color="primary" 
        class="submit-button" 
        aria-label="LOGIN"    
        [disabled]="loginForm.invalid" 
        (click)="onLoginWithEmail()">
  LOGIN
</button>

Регистрация или влизане с...

След нашия формулярелемент искаме да дадем на потребителя опцията да създаде акаунт или да използва Google за влизане. HTML/CSS е прост. Промених CSS в този „въпрос“ на StackOverflow, за да получа центриран текст ИЛИ с линии. Можете да изтеглите този клон на хранилището в края на статията. Вижте подробностите за CSS във файла login.component.css

Услуга за удостоверяване

Сега трябва да създадем услуга, която ще обработва удостоверяване с Firebase. Създайте нов файл authenication.service.ts в директорията src\app\shared\services. За да покажем всякакви грешки, ще използваме нашата услуга за известия и в нашия конструктор ще я присвоим на свойство, наречено notifier

import { Injectable, Injector } from '@angular/core';
// Services
import { NotificationService } from './notification.service';
@Injectable({ providedIn: 'root' })
export class AuthService {
    notifier: NotificationService;
constructor(
        private injector: Injector
    ) {
        this.notifier = this.injector.get(NotificationService);
    }
}

Нека се съсредоточим върху другите модули, които трябва да импортираме в нашата услуга. Искаме да използваме Firebase/AngularFire и след успешно удостоверяване ще навигираме към различен маршрут. Нашата секция за импортиране сега ще изглежда така:

import { Injectable, Injector } from '@angular/core';
import { Router } from '@angular/router';
// Firebase
import { firebase } from '@firebase/app';
import '@firebase/auth';
// Angularfire2
import { AngularFireAuth } from '@angular/fire/auth';
import {
    AngularFirestore, 
    AngularFirestoreDocument 
} from '@angular/fire/firestore';
// Services
import { NotificationService } from './notification.service';
...

След импортирането на услугите ще дефинираме нашия потребителски интерфейс и ще го присвоим на свойство, наречено user в нашия клас AuthService. Нека също добавим свойство, наречено isLoading, което ще използваме, за да покажем индикатор за напредък по време на удостоверяването:

// Classes / Interfaces
interface IUser {
    uid: string;
    email: string;
    photoURL?: string;
    displayName?: string;
    emailVerified: boolean;
}
@Injectable({ providedIn: 'root' })
export class AuthService {
    notifier: NotificationService;
    user: IUser;
    isLoading: boolean;
constructor(
        private injector: Injector
...

След това ще инжектираме нашите други зависимости в конструктора:

constructor(
    private afAuth: AngularFireAuth,
    private afs: AngularFirestore,
    private router: Router,
    private injector: Injector
) {
...

влизане с имейл

async loginWithEmail(email: string, password: string): Promise<void> {
    await this.afAuth.auth.signInWithEmailAndPassword(
        email, 
        password
    ).then(credential => {
        this.updateUserData(credential.user).then(() => {
            this.getCurrentUser();
        }, error => {
            this.isLoading = false;
            this.notifier.showError(error.message);
        });
    }).catch((error) => {
        this.notifier.showError(error.message);
    });
}

Поглеждайки назад към файла,login.component.ts ще трябва да импортираме нашата AuthService:

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
// Services
import { AuthService } from 'src/app/shared/services/authentication.service';

Включете това в нашия конструктор и създайте нашия метод за кликване, който извиква новата услуга. Докато сме там, нека добавим и метода за щракване, за да обработваме нашето влизане в Google:

constructor(
  private formBuilder: FormBuilder,
  public auth: AuthService
) { }
...
onLoginWithEmail() {
  this.auth.loginWithEmail(
    this.loginForm.value.email,
    this.loginForm.value.password
  );
}
onLoginWithGoogle() {
  this.auth.loginWithGoogle();
}

Все още имаме два метода в нашата услуга, които трябва да бъдат създадени, така че нека ги завършим.

updateUserData

Този метод не е задължителен, но ще се използва за препратка към документ във Firestore, за да се запази информацията за потребителя, след като бъде удостоверен.

async updateUserData({ 
    uid, 
    email, 
    displayName, 
    photoURL, 
    emailVerified }: IUser, 
    registrationName?: string
) {
    const userRef: AngularFirestoreDocument<IUser> = this.afs.doc(`users/${uid}`);
    const data: IUser = {
      uid,
      email,
      displayName: (registrationName) ? registrationName : displayName,
      photoURL,
      emailVerified: emailVerified
    }
    return await userRef.set(data, { merge: true });
}

Предаваме информацията за интерфейса IUser и получаваме препратка към данните на документа, задаваме стойностите на нашата променлива с данни и накрая изпълняваме метода set с опцията { merge: true }.

Ако не сте сигурни дали документът съществува, подайте опцията за обединяване на новите данни с всеки съществуващ документ, за да избегнете презаписването на цели документи.

getCurrentUser

След като съхраним информацията във Firestore, ще искаме да проверим дали имаме тази информация, съхранена в потребителското localStorage.

async getCurrentUser() {
    // get the local storage information by key
    const localUser = JSON.parse(localStorage.getItem('user'));
    if (localUser === null) {
        // information not stored in local storage; 
        // so get the authentication state from AngularFireAuth
        await this.afAuth.authState.subscribe(user => {
            if (user) {
                // user was authenitcated, set values
                this.user = {
                    'uid': user.uid,
                    'email': user.email,
                    'photoURL': user.photoURL,
                    'displayName': user.displayName,
                    'emailVerified': user.emailVerified
                };
                // store information locally
                localStorage.setItem('user', JSON.stringify(this.user));
                // route authenticated user to appropriate page
                if (this.router.url === '/auth/login') {
                    // user logging in, so take them to default page
                    this.router.navigate(['']);
                }
                // hide the progress spinner
                this.isLoading = false;
            } else {
                // user not authenticated, so NULL the value
                this.user = null;
                // store null information locally
                localStorage.setItem('user', JSON.stringify(this.user));
                // hide the progress spinner
                this.isLoading = false;
            }
        });
    } else {
        // information already stored locally, 
        // so set to user variable
        this.user = localUser;
        // hide the progress spinner
        this.isLoading = false;
    }
}

влизане с Google

Firebase прави влизането със социални доставчици доста лесно, така че нека добавим това към нашата услуга:

async loginWithGoogle(): Promise<void> {
    const provider = new firebase.auth.GoogleAuthProvider();
    provider.addScope('profile');
    provider.addScope('email');
    await this.afAuth.auth.signInWithRedirect(provider);
}

Какво еsignInWithRedirect? С нашата логика по-горе, потребителят щраква върху нашия бутон Google под „ВХОД С“, той се пренасочва към входа на Google, където или ще избере акаунт, с който е влязъл, или ще въведе своите идентификационни данни. След като бъдат удостоверени, те се пренасочват обратно към нашата страница за вход. Как да се справим с това пренасочване и да проверим дали са удостоверени?

verifyUserRedirect

Имаме нужда от асинхронен метод, за да извикаме нашия конструктор на услуги, за да проверим за пренасочване. Отново Firebase прави това доста просто с метода getRedirectResult().

async verifyUserRedirect(): Promise<void> {
    // show the progress spinner
    this.isLoading = true;
    await firebase.auth().getRedirectResult().then(auth => {
        // user property exists; this was a redirect
        if (auth.user) {
            this.updateUserData(auth.user).then(() => {
                this.getCurrentUser();
            }, error => {
                // hide the progress spinner
                this.isLoading = false;
                // user our notifier to show any errors
                this.notifier.showError(error.message);
            });
        } else {
            // this was not a redirect; 
            // use our method to check for user
            // in local storage
            this.getCurrentUser();
        }
    }, (error) => {
        // hide the progress spinner
        this.isLoading = false;
        // user our notifier to show any errors
        this.notifier.showError(error.message);
    });
}

Навигация / Изход

В зависимост от състоянието на удостоверяване на нашия потребител, трябва или да го пренасочим към нашата страница за вход, или да му покажем падащо меню, за да могат да изберат да излязат.

authentication.service.ts

Единствената логика, която трябва да добавим към нашата услуга, е методът logout и отново Firebase прави това доста просто с вграден метод:

async logout(): Promise<void> {
    // show the progress spinner
    this.isLoading = true;
    await this.afAuth.auth.signOut().then(() => {
        // clear the user information and local storage
        this.user = null;
        localStorage.clear();
        // hide the progress spinner
        this.isLoading = false;
    }, error => {
        // use our notifier to show any errors
        this.notifier.showError(error.message);
        // hide the progress spinner
        this.isLoading = false;
    });
}

nav.component.ts

Първо, нека импортираме два модула. Нуждаем се от модула Router, за да насочваме нашите потребители към различните страници и също се нуждаем от нашия AuthService, за да можем да извикаме нашия метод за излизане.

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../../services/authentication.service';
...

Не забравяйте да ги добавите към конструктора

constructor(
    public auth: AuthService,
    private router: Router,
) { }

И накрая, ще ни трябва един метод goToLogin(), който ще насочва потребителите към онази фантастична страница за вход, която току-що създадохме, и onLogout(), който ще използва нашия новосъздаден метод, който излиза от текущия потребител от Firebase.

...
ngOnInit() {
}
goToLogin() {
    // routes the user to login page
    this.router.navigate(['auth/login']);
}
onLogout() {
    this.auth.logout().then(() => {
        // routes the user to root page
        this.router.navigate(['']);
    });
}

nav.component.html

Нашият HTML ще види по-голямата част от промените. Трябва да добавим нашето събитие за кликване goToLogin() към нашия съществуващ бутон, за да можем да ги навигираме до страницата за вход и искаме да показваме този бутон само ако не са удостоверени *ngIf="!auth.user"

<span fxFlex></span>
<button mat-icon-button *ngIf="!auth.user" (click)="goToLogin()">
    <mat-icon>account_circle</mat-icon>
</button>

Под този бутон ще добавим div, който ще се показва само когато потребителят е удостоверен *ngIf="auth.user", а вътре в това div ще имаме бутон, който или ще показва снимката на удостоверения потребител, ако съществува, или mat-icon с иконата на човек. Ние ще създадем Angular Material Menu, така че нека добавим директивата matMenuTriggerFor и да я прикрепим към нашето mat-меню, което ще създадем след това.

<div *ngIf="auth.user">
    <button mat-icon-button [matMenuTriggerFor]="userMenu">
        <img class="avatar-photo" 
            *ngIf="auth.user?.photoURL" 
            [src]="auth.user?.photoURL" />
        <mat-icon 
            *ngIf="!auth.user?.photoURL">person</mat-icon>
    </button>
</div>

Нашият mat-menu отива под бутона и ще има опция от менюто, която позволява на потребителя да излезе от системата с нашия onLogout() метод. Също така искаме да прикрепим менюто към нашия matMenuTriggerFor, като добавим #userMenu="matMenu". Тъй като менюто ни е от дясната страна, нека зададем и xPosition="before", така че менюто да се показва вляво.

<mat-menu #userMenu="matMenu" xPosition="before">
    <ng-template matMenuContent>
        <!--
        <button mat-menu-item 
                [routerLink]="'/user/settings'">
            Settings
        </button>
        -->
        <button mat-menu-item (click)="onLogout()">Logout</button>
    </ng-template>
</mat-menu>

nav.component.css

Единственият CSS, от който се нуждаем в момента за нашата навигация, е стилът за снимката на аватара.

.avatar-photo {
    height: 35px;
    width: 35px;
    border-radius: 50%;
}

login.module.ts

Мързеливи сме да зареждаме нашия auth маршрут, но имаме нужда от него, за да знаем за новия ни маршрут за влизане. Създайте файл login.module.ts във вашата директория src\app\pages\authentication\login със следното съдържание:

import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { SharedModule } from 'src/app/shared/shared.module';
import { LoginComponent } from './login.component';
const routes = [{
    path: 'login',
    component: LoginComponent
}];
@NgModule({
    declarations: [LoginComponent],
    imports: [
        SharedModule,
        RouterModule.forChild(routes)
    ]
})
export class LoginModule {}

Импортирахме нашия SharedModule, така че нашият LoginComponent да може да използва FlexLayout, ReactiveForms и т.н. Настроихме нашия routes с помощта на .forChild, защото искаме те да бъдат вложени в нашия auth път.

authentication.module.ts

Пътят localhost:4200/auth ни отвежда до: authentication.module.ts, но как да уведомим нашето приложение за нашия вложен път localhost:4200/auth/login? Всичко, което трябва да направим, е да импортираме нашия LoginModule.

import { NgModule } from '@angular/core';
import { LoginModule } from './login/login.module';
@NgModule({
    imports: [
        LoginModule
    ]
})
export class AuthenticationModule {}

Spinner за материален прогрес

Почти забравихме за нашата собственост isLoading в нашата authentication.service. Ще използваме Material Spinner, когато приложението ни комуникира с Firebase, така че потребителят да знае, че нещо се случва на заден план. Всичко, което трябва да направим, е да добавим HTML към нашия src\app\app.component.html файл. Когато не сме auth.isLoading, ще покажем <router-outlet> по подразбиране

<div class="page-wrapper" *ngIf="!auth.isLoading">
  <router-outlet></router-outlet>
</div>

Под това <div> ще добавим нашето <mat-spinner> и ще покажем това само когато се извършва удостоверяване:

<div fxLayout 
     fxLayoutAlign="center center" 
     style="background-color: #555; min-height: 100vh" 
     *ngIf="auth.isLoading">
  <mat-spinner color="accent"></mat-spinner>
</div>

app.module.ts

Сега трябва да отворим filesrc\app\app.module.ts и да инжектираме нашия authentication.service в нашите доставчици на приложения.

...
// Environments
import { environment } from '../environments/environment';
// Services
import { AuthService } from './shared/services/authentication.service';
// Providers
import { ErrorsHandler } from './shared/providers/error-handler';
import { HttpsInterceptor } from './shared/providers/http-interceptor';
...
@NgModule({
  ...
  providers: [
    AuthService,
    { provide: ErrorHandler, useClass: ErrorsHandler },
    { provide: HTTP_INTERCEPTORS, useClass: HttpsInterceptor, multi: true }
  ],
  ...
})
export class AppModule { }

app.component.ts

И накрая, ние извиквамеauth.isLoading в нашия app.component, така че трябва да импортираме услугата и да добавим препратка към нашия конструктор

import { Component } from '@angular/core';
import { AuthService } from './shared/services/authentication.service';
...
export class AppComponent {
  constructor(
    protected auth: AuthService
  ) {}
  ...
}

Клон хранилище

Доставчиците за влизане са деактивирани за този проект, така че ако следвате, като клонирате моите хранилища, ще получите грешка по време на влизане



Заключение

Това е отлично място за почивка. Чувствайте се свободни да вземете лека закуска или чаша ☕️. Ако имате въпроси, коментари или предложения за моите истории, уведомете ме.

Благодаря ви, че отделихте време да прочетете моите истории и ако сте последовател, аз съм благодарен ❤️. Ако ви е харесала тази статия, моля, помислете дали да не задържите този бутон за аплодиране 👏!