Angular & Firebase Authentication part 2

Phai Panda
7 min readApr 6, 2020

--

จากเดิมเรามี web app อยู่แล้ว แต่ยังไม่มีระบบสมัครสมาชิก (register user) ไม่มีระบบจัดการสมาชิก (manage user) ครั้งนี้เราจะมาเล่าและเขียนเรื่องนี้ร่วมกัน การสมัครสมาชิกจะใช้แค่ email กับ password ครับ

โดยปกติทั่วไปสำหรับ web app ที่มีระบบบริการบางอย่างทางด้าน business จำต้องมีระบบสมัครสมาชิกเพื่อระบุตัวตน ติดตาม ให้ข้อเสนอหรือสิทธิ์ในการเยี่ยมชมหรือปฏิบัติงานในแต่ละหน้าของ web app บทความนี้จะนำเสนอต่อเนื่องจาก part ที่ผ่านมา

โดยพระเอกยังคงเป็น AngularFire

ng-bootstrap

หนนี้เราจะแต่ง CSSให้สวย มันจะได้เป็นหน้าเป็นตาหน่อย (อันนี้นอกเรื่อง) เลือกเอายี่ห้อตลาดทั่วไปแล้วกันครับ ผมขอเลือกเป็น ng-bootstrap

  • ขณะนี้เวอร์ชัน v6.0.2
  • เวอร์ชันนี้ 6.x.x เข้ากันได้กับ Angular 9.0.0 และเข้ากันได้กับ Bootstrap 4.4.1
  • เขาบอกว่าถ้าใช้ Angular ≥ 9.0.0 และ ng-bootstrap ≥ 6.0.0, เราต้องติดตั้ง @angular/localize polyfill ด้วย
  • bootstrap.js หรือ bootstrap.min.js ไม่มีความจำเป็นต้องเอาเข้ามา
  • jQueryหรือ popper.jsก็ไม่ต้องเอาเข้ามา
  • ขอแค่ bootstrap ธรรมดาก็พอ (bootstrap css)

กลับมาถามว่าโปรเจกต์ตั้งต้นนี้มีสิ่งที่มันต้องการแล้วหรือยัง

  • เราใช้ Angular 9 อยู่แล้ว
  • เราใช้ Bootstrap สำหรับ CSS อยู่แล้ว
  • เราได้ import bootstrap.css ที่ไฟล์ styles.css แล้วด้วย

ดังนั้นติดตั้งสิ่งที่ขาดครับ เริ่มจาก

ng add @angular/localize

ตามด้วย

npm install @ng-bootstrap/ng-bootstrap

Basic Authentication by Email & Password

ผมอยากได้ dialog ที่กำหนดด้วย 2 ส่วน ซ้าย กับ ขวา

  • ซ้าย ใช้สำหรับ Sign In
  • ขวา ใช้สำหรับ Create Account

สร้างขึ้นมา

ng g c components/sign-in

และ

ng g c components/sign-up

และ

ng g c dialogs/sign-in-dialog

sign-in กับ sign-up ตั้งใจนำไปใช้กับ sign-in-dialog

Create Account

เปิดไฟล์ sign-up.component.html

  • ต้องการช่องป้อน email
  • ต้องการช่องป้อน password
  • ต้องการช่องป้อน repassword
  • ต้องการปุ่ม create account และยกเลิก

หมายเหตุ ขณะนี้ยังอยู่ในขั้นตอนการ dev กล่าวคือ แต่ละหน้าที่ต้องการวาดและออกแบบเราควรได้เห็นไวๆ เขียนและแก้ไขไวๆ โดยเฉพาะกับไฟล์ .html ด้วยเหตุนี้เราจะใช้หน้า app.component.html มาทำแทนก่อน เมื่อได้ตามต้องการแล้วเราจึงย้ายมันไปยัง .html ของมันเอง

โชคดีที่การ generate component จะมี selector เสมอ เราจะใช้มันไปวางไว้หน้า app.component.html แทนการวาด tags ของ component นั้นๆลงไปตรงๆ

sign-up.component.ts มี selector ชื่อ app-sign-up ให้นำมันไปวางไว้หน้า app.component.html

... // this is app.component.html<router-outlet></router-outlet><app-sign-up></app-sign-up>

ไฟล์ sign-up.component.html

<form [formGroup]="form">  <div class="form-group">    <label for="signUpEmail">Email</label>    <input type="email" class="form-control" id="signUpEmail" formControlName="email" placeholder="enter email">  </div>  <div class="form-group">    <label for="signUpPassword">Password</label>    <input type="password" class="form-control" id="signUpPassword" formControlName="password"placeholder="enter password" autocomplete="off">  </div>  <div class="form-group">    <label for="signUpRePassword">Re Password</label>    <input type="password" class="form-control" id="signUpRePassword" formControlName="repassword"placeholder="enter password again" autocomplete="off">  </div>  <div class="float-right">    <button class="btn btn-primary mr-3" (click)="onClickedSignUp()">create account</button>    <button class="btn btn-secondary" (click)="onClickedCancel()">cancel</button>  </div></form>

ไฟล์ sign-up.component.ts

...form: FormGroup;constructor(  private fb: FormBuilder,) { }ngOnInit(): void {  this.form = this.fb.group({    email: ['', Validators.email],    password: [''],    repassword: [''],  });}onClickedSignUp() { }onClickedCancel() { }...

:)

Sign In

ที่ app.component.html ลบ <app-sign-up></app-sign-up> ออก เปลี่ยนเป็น <app-sign-in></app-sign-in>

... // this is app.component.html<router-outlet></router-outlet><app-sign-in></app-sign-in>

เปิดไฟล์ sign-in.component.html

  • ต้องการช่องป้อน email
  • ต้องการช่องป้อน password
  • ต้องการปุ่ม sign in และยกเลิก
<form [formGroup]="form">  <div class="form-group">    <label for="signInEmail">Email</label>    <input type="email" class="form-control" id="signInEmail" formControlName="email" placeholder="enter email">  </div>  <div class="form-group">    <label for="signInPassword">Password</label>    <input type="password" class="form-control" id="signInPassword" formControlName="password"placeholder="enter password" autocomplete="off">  </div>  <div class="float-right">    <button class="btn btn-primary mr-3" (click)="onClickedSignIn()">sign in</button>    <button class="btn btn-secondary" (click)="onClickedCancel()">cancel</button>  </div></form>

ไฟล์ sign-in.component.ts

...form: FormGroup;constructor(  private fb: FormBuilder,) { }ngOnInit(): void {  this.form = this.fb.group({    email: ['', Validators.email],    password: [''],  });}onClickedSignIn() { }onClickedCancel() { }...

Sign In Dialog

เมื่อได้ทั้ง Create Account และ Sign In สองหน้านี้แล้ว ก็นำมามารวมกันที่หน้า sign-in-dialog.component.html

ที่ app.component.html ลบ <app-sign-in></app-sign-in> ออก เปลี่ยนเป็น <app-sign-in-dialog></app-sign-in-dialog>

... // this is app.component.html<router-outlet></router-outlet><app-sign-in-dialog></app-sign-in-dialog>

ด้วย ng-bootstrap ผมเลือกใช้ Tabset (แม้เขาจะบอกว่า deprecated แล้วตั้งแต่เวอร์ชัน 6.0.0) เป็นตัวเชื่อมระหว่าง Create Account กับ Sign In

เปิดไฟล์ app.module.ts เพิ่ม

import { NgbTabsetModule } from '@ng-bootstrap/ng-bootstrap';

และ

imports: [...  NgbTabsetModule,...],

กลับมาที่ไฟล์ sign-in-dialog.component.html

<div class="p-5">  <ngb-tabset [destroyOnHide]="false">    <ngb-tab title="Sign In">      <ng-template ngbTabContent>        <div class="py-3">          <app-sign-in></app-sign-in>        </div>      </ng-template>    </ngb-tab>    <ngb-tab title="Create Account">      <ng-template ngbTabContent>        <div class="py-3">          <app-sign-up></app-sign-up>        </div>      </ng-template>    </ngb-tab>  </ngb-tabset></div>

ผลที่ได้

create sign in dialog

:)

Call Sign In Dialog

ผมจะสร้างปุ่ม sign in ไว้ที่หน้า app.component.html เพื่อว่าผู้ใช้หากยังไม่ได้เข้าสู่ระบบจะต้องเห็นปุ่มนี้

เปิดไฟล์ app.component.html

เพิ่มปุ่มลงไปและลบ tag ที่ไม่ต้องการออก

<h1>{{title | uppercase}}</h1><div>  <a routerLink="realtimedb/trailers">Realtime Database</a>  | <a routerLink="cloudfirestore/trailers">Cloud Firestore</a>  | <a routerLink="cloudstorage/videos">Cloud Storage</a></div><router-outlet></router-outlet><button (click)="openSignInDialog()">Sign In</button>

เปิดไฟล์ app.module.ts เพิ่ม

import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap';

และ

imports: [...  NgbModalModule,...],

เปิดไฟล์ app.component.ts เพิ่มฟักง์ชัน openSignInDialog

ที่ constructor ของคลาส AppComponent

import { NgbModal } from '@ng-bootstrap/ng-bootstrap';import { SignInDialogComponent } from './dialogs/sign-in-dialog/sign-in-dialog.component';

และ

constructor(  private modal: NgbModal,) { }

และ

openSignInDialog() {  this.modal.open(SignInDialogComponent);}

ผลลัพธ์

create sign in button

และเพื่อให้ dialog นี้สามารถปิดตัวเองได้ ให้เพิ่มโค้ดการเรียก close dialog ที่ 2 ไฟล์นี้

ไฟล์ sign-in.component.ts ฟังก์ชัน onClickedCancel

ไฟล์ sign-up.component.ts ฟังก์ชัน onClickedCancel

โดย

import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';

และ

constructor(  private fb: FormBuilder,  private activeModal: NgbActiveModal,) { }

และ

onClickedCancel() {  this.activeModal.close();}

ผลลัพธ์

can close dialog by click cancel button right now

:)

ตรวจสอบการแสดงปุ่ม sign in ปุ่มนี้จะปรากฏก็ต่อเมื่อผู้ใช้ยังไม่ได้เข้าสู่ระบบ

กลับมาที่ไฟล์ app.component.ts เพิ่มการฟัง auth state

import { AngularFireAuth } from '@angular/fire/auth';import { User } from 'firebase';import { Observable } from 'rxjs';

และ

constructor(  private modal: NgbModal,  private auth: AngularFireAuth,) { }

และ

export class AppComponent implements OnInit { ... }

และ

export class AppComponent implements OnInit {  title = 'oxygen-not-included-wiki';  authState$: Observable<User>;...

และ

ngOnInit() {  this.authState$ = this.auth.authState;}

เปิดไฟล์ app.component.html เพื่อจัดวางโครงสร้างหน้านี้ใหม่

<h1>{{title | uppercase}}</h1><div *ngIf="authState$ | async as user; else signIn">  <div>    <a routerLink="realtimedb/trailers">Realtime Database</a>    | <a routerLink="cloudfirestore/trailers">Cloud Firestore</a>    | <a routerLink="cloudstorage/videos">Cloud Storage</a>  </div>  <router-outlet></router-outlet></div><ng-template #signIn>  <button (click)="openSignInDialog()">Sign In</button></ng-template>

ผล

sign in before

:)

Create an Account with Email and Password

ด้วยความสามารถของ AngularFireAuth อย่างที่เพื่อนๆทราบมาแล้วจาก part แรก เขามีบริการ createUserWithEmailAndPassword เราก็หยิบมาใช้แค่นั้น

เปิดไฟล์ sign-up.component.ts

  • ที่ฟังก์ชัน onClickedSignUp เราจะเพิ่มการสมัครสมาชิกด้วย email และ password
  • โดยปกติแล้ว password จะต้องได้รับการยืนยันด้วย re password คือมันทั้งคู่จะต้องเหมือนกันเพื่อป้องกันไม่ให้ผู้ใช้สร้าง password ที่ตนเองจำไม่ได้ (แต่ก็คัดลอกและวางได้อยู่ดี)
  • ขอตัดเรื่องการ validation ทั้งหมดออกไปก่อน รวมถึงการตรวจสอบ password กับ re password ด้วย
  • เราจะสร้างฟังก์ชันย่อยอีกเล็กน้อยเพื่อให้การใช้ฟิวด์ของฟอร์มง่ายขึ้น (เรื่องนี้เป็นของ Angular Form)
get email(): AbstractControl { return this.form.controls.email; }get password(): AbstractControl { return this.form.controls.password; }get repassword(): AbstractControl { return this.form.controls.repassword; }

จากนั้นนำ AngularFireAuth เข้ามา

import { AngularFireAuth } from '@angular/fire/auth';

และ

constructor(  private fb: FormBuilder,  private activeModal: NgbActiveModal,  private auth: AngularFireAuth,) { }

และ

onClickedSignUp() {  const email = this.email.value;  const password = this.password.value;  this.auth.createUserWithEmailAndPassword(email, password).then(() => {    alert('Your Account is Created!');    this.activeModal.close();  });}

ทดสอบ

test create an account

ผล

Error: Uncaught (in promise): Error: Password should be at least 6 characters

มันบอกว่า password ควรมีความยาวมากกว่า 6 อักษร

เอาใหม่

ผล

Error: Uncaught (in promise): Error: The given sign-in provider is disabled for this Firebase project. Enable it in the Firebase console, under the sign-in method tab of the Auth section.

ไม่สามารถสมัครได้เพราะ Firebase ยังไม่อนุญาตให้ทำได้

แก้ไข ตรงไปยัง Firebase เลือกโปรเจกต์และ Authentication

ที่ Sign-in method มองหา Email/Password

look at Sign-in method tab

จากนั้นเลือก Enable แล้วคลิก Save

enable email/password

กลับมาทดสอบใหม่

สำเร็จ!
เมื่อเข้าสู่ระบบแล้วก็ไม่ปรากฏปุ่ม sign in
เห็นผู้ใช้ที่เพิ่งสมัครด้วย

:)

Sign Out Button

ขั้นตอนการออกจากระบบนั้นง่ายมาก ผมจะสร้างปุ่ม sign out ไว้บนมุมขวาของ app.component.html เมื่อใดที่ผู้ใช้ได้เข้าสู่ระบบแล้วปุ่มนี้จึงจะแสดงตัวออกมา

เราจะใช้กลไกเดิม นั่นคือฟัง auth state แต่เป็นการฟังแบบตรงกันข้ามกับปุ่ม sign in

เปิดไฟล์ app.component.html

<div class="d-flex">  <h1>{{title | uppercase}}</h1>  <div *ngIf="authState$ | async" class="ml-auto">    <button (click)="onClickedSignOut()">Sign Out</button>  </div></div><div *ngIf="authState$ | async as user; else signIn">...

เขียนฟังก์ชัน onClickedSignOut

เปิดไฟล์ app.component.ts

onClickedSignOut() {  this.auth.signOut();}

ผลลัพธ์

create sign out button

ทดสอบออกจากระบบ

now you should sign in again

:)

Sign In with Email and Password

ก็มาถึงอย่างสุดท้ายของบทความนี้แล้ว นั่นคือการเข้าสู่ระบบด้วย email กับ password จินตนาการได้ว่าคงไม่ยากเย็นอะไรอีกต่อไป

เมื่อคลิกที่ปุ่ม sign in จะปรากฏ sign in dialog

ดังนั้นเปิดไฟล์ sign-in.component.ts

เพิ่มฟังก์ชันย่อยเพื่อเรียกใช้ฟิวด์ของฟอร์มง่ายขึ้น

get email(): AbstractControl { return this.form.controls.email; }get password(): AbstractControl { return this.form.controls.password; }

เพิ่มโค้ดฟังก์ชัน onClickedSignIn

import { AngularFireAuth } from '@angular/fire/auth';

และ

constructor(  private fb: FormBuilder,  private activeModal: NgbActiveModal,  private auth: AngularFireAuth,) { }

และ

onClickedSignIn() {  const email = this.email.value;  const password = this.password.value;  this.auth.signInWithEmailAndPassword(email, password).then(() => {    this.activeModal.close();  });}

ทดสอบ

ผล

สำเร็จ!

หมายเหตุ ผมเขียนคำว่า sign เป็น sing อยู่บ่อย ภายหลังตรวจพบก็แก้ไข เป็นเหตุให้ภาพที่ใช้ประกอบแตกต่างกันบ้างเล็กน้อยนะครับ

เพื่อนๆจะเห็นว่า Firebase นั้นทรงพลังมาก แถมยังมี APIs ที่ดีที่ช่วยให้เราพัฒนา application ได้ง่ายและรวดเร็ว ขอมีทักษะแค่ Angular ก็สามารถเขียน web application อย่างบทความนี้ได้แล้ว

อ้างอิง

https://firebase.google.com/docs/auth/web/start

https://firebase.google.com/docs/auth/web/password-auth

https://www.chromium.org/developers/design-documents/form-styles-that-chromium-understands

--

--