Angular & Firebase Authentication part 2
จากเดิมเรามี 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>
ผลที่ได้
:)
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);}
ผลลัพธ์
และเพื่อให้ 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();}
ผลลัพธ์
:)
ตรวจสอบการแสดงปุ่ม 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>
ผล
:)
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(); });}
ทดสอบ
ผล
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
จากนั้นเลือก Enable แล้วคลิก Save
กลับมาทดสอบใหม่
:)
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();}
ผลลัพธ์
ทดสอบออกจากระบบ
:)
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