Ionic + Firebaseで記録するカレンダーアプリを作る　その5 ユーザー作成と認証

#完成イメージ
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">こんな感じ。 <a href="https://t.co/CvOFjFq2th">pic.twitter.com/CvOFjFq2th</a></p>&mdash; げん げんと (@gento34165638) <a href="https://twitter.com/gento34165638/status/1190431795877007362?ref_src=twsrc%5Etfw">November 2, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

#はじめに
環境は以下の通り
・ionic4
・Nativeとの橋渡しには[capacitor](https://capacitor.ionicframework.com)を使用
・カレンダーについては[ionic2-calendar](https://github.com/twinssbc/Ionic2-Calendar#readme)というライブラリーを使用

前回はdayページからイベントを追加、更新、削除ができるようになった。
:point_right:前回の記事は[こちら](https://qiita.com/kokogento/items/c1c3c78db5a6e58f8a08)

・フォームの入力チェックについては以下を参考に
[ionic フォームで変なこと書いてないか、バリデーションを実装](https://qiita.com/kokogento/items/e1e152814153c21936fe)

・ログイン時のFirebase側でのエラーを表示する方法については以下を参考に
[Firebase ionic サインアップのエラータイプを判別して、アラート表示を変える](https://qiita.com/kokogento/items/8d84054052d3daa05127)
#概要
このアプリは**各アカウントがイベントを持つ事**で、ようやく自分以外の人でも使えるようになる。そうでないと全ユーザーが共通のカレンダーを見る事になるからね・・・

Ionic4 + Firebaseでのユーザー認証は[こちら](https://qiita.com/se_fy/items/386418f123fc30c306e6)で詳しく解説されている。
私自身もこの記事を参考に作った。

今回はとりあえず**ユーザーを作成して、そのユーザーでログイン**できるようになる所までやってみる。

ログインしているユーザーごとに、表示するカレンダーを変えるのは次回という事で。

#FirebaseコンソールでAuthenticationを設定
メールアドレスとパスワードでの認証を有効にする。

<img width="639" alt="スクリーンショット 2019-11-12 19.21.25.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167220/19cad968-8ae1-3745-ac2a-d34dae0a95c1.jpeg">




#必要なモジュールをapp.module.tsで登録
`AngularFireAuthModule`をimportして`@NgModule`に追加

```typescript:app.module.ts
import { NgModule, LOCALE_ID } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { HttpClientModule } from '@angular/common/http';

//カレンダー関係
import { NgCalendarModule  } from 'ionic2-calendar';
import { registerLocaleData } from '@angular/common';
import localeJa from '@angular/common/locales/ja';

//Firebaseを利用するためのモジュール
import { AngularFireModule } from '@angular/fire';
import { AngularFirestoreModule } from '@angular/fire/firestore';
import { AngularFireAuthModule } from '@angular/fire/auth';

//Firebase設定情報ファイルをインポート
import { environment } from '../environments/environment';

registerLocaleData(localeJa);


@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [
    BrowserModule,
    IonicModule.forRoot(),
    AppRoutingModule,
    NgCalendarModule,
    HttpClientModule,
    AngularFireModule.initializeApp(environment.firebase),
    AngularFirestoreModule,
    AngularFireAuthModule
  ],
  providers: [
    StatusBar,
    SplashScreen,
    NativeStorage,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    { provide: LOCALE_ID, useValue: 'ja-JP' }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}
```

#authページを作る
本当は素直に`loginページ`と呼びたい所だが、認証関係の処理を`homeページ`でもやったように**serviceにまとめたい。**

ログイン以外にも認証（Authentication）したい時は出てくるので、ここでは`authページ`として作成したいと思う。

```terminal
ionic g page auth
```

##auth.service.ts
`ionic g service auth/auth`

```typescript:auth.service.ts
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { AngularFireAuth } from '@angular/fire/auth';
import { Observable } from 'rxjs';
import { AngularFirestore, AngularFirestoreDocument } from '@angular/fire/firestore';
import { switchMap } from 'rxjs/operators';
import { LoadingController, AlertController, ToastController } from '@ionic/angular';

export interface User {
  uid: string;
  email: string;
  displayName?: string;
  myCustomData?: string;
  photoURL?: string;
}

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  user$: Observable<User[]>;

  constructor(
    private router: Router,
    private afs: AngularFirestore,
    private afAuth: AngularFireAuth,
    public loadingCtrl: LoadingController,
    private alertCtrl: AlertController,
    private toastCtrl: ToastController
  ) {
    this.user$ = this.afAuth.authState.pipe(
      switchMap(user => {
        if (user) {
          this.updateUserData(user);
          return this.afs.doc<User[]>(`users/${user.uid}`).valueChanges();
        } else {
          this.router.navigateByUrl('/auth');
        }
      })
    );
  }

  async login(email: string, password: string) {
    await this.afAuth.auth
      .signInWithEmailAndPassword(email, password)
      .then(async user => {
        this.router.navigateByUrl('/home');
        const toast = await this.toastCtrl.create({
          message: `${this.afAuth.auth.currentUser.displayName}さん、こんにちは！`,
          duration: 3000
        });
        await toast.present();
      })
      .catch(error => {
        console.log(error);
        const code = error.code;
        const header = 'ログイン失敗';
        let message = 'エラーが発生しました。もう一度お試しください。。。';
        if (code === 'auth/user-not-found') {
          message =
            'ユーザーが見つかりません。メールアドレスを確認して下さい。。。';
        } else if (code === 'auth/wrong-password') {
          message =
            'そのメールアドレスは存在していますが、パスワードが違います。。。';
        }
        this.showAlert(header, message);
        this.router.navigateByUrl('/auth');
      });
  }

  private showAlert(header: string, message: string) {
    this.alertCtrl
      .create({
        header: header,
        message: message,
        buttons: ['OK']
      })
      .then(alertEl => alertEl.present());
  }

  async logout() {
    this.afAuth.auth.signOut();
  }

  async presentLoading(loading) {
    return await loading.present();
  }

  updateUserData(user) {
    // ログイン時にユーザーデータをfirestoreに設定
    // usersコレクションの下に入る
    const userRef: AngularFirestoreDocument<User> = this.afs.doc(
      `users/${user.uid}`
    );
    const data = {
      uid: user.uid,
      email: user.email,
      displayName: user.displayName,
      photoURL: user.photoURL
    };
    return userRef.set(data, { merge: true });
  }

  getUser(): Observable<User[]> {
    return this.user$;
  }
}
```


##auth.page.html
**実質上のログインページ**

```html:auth.page.html
<ion-header>
  <ion-toolbar color="primary">
    <ion-title>ログイン</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>
  <ion-grid>
    <ion-row>
      <ion-col size="12" size-sm="8" offset-sm="2">
        <ion-item class="loginHeader" lines="none" text-center>
        </ion-item>
        <form #f="ngForm" class="loginForm">
          <ion-item class="formItem">
            <ion-label position="floating">
              <ion-icon name="mail"></ion-icon>
              メールアドレス
            </ion-label>
            <ion-input required email [(ngModel)]="login.email" type="email" name="login.email" #emailCtrl="ngModel">
            </ion-input>
          </ion-item>
          <ion-item *ngIf="!emailCtrl.valid && emailCtrl.touched" lines="none">
            <ion-label color="danger">メールアドレスを入力してね！</ion-label>
          </ion-item>
        
          <ion-item class="formItem">
            <ion-label position="floating">
              <ion-icon name="key" padding-right></ion-icon>パスワード
            </ion-label>
            <ion-input required [(ngModel)]="login.password" type="password" name="login.password" #passwordCtrl="ngModel"
              minlength="6">
            </ion-input>
          </ion-item>
          <ion-item *ngIf="!passwordCtrl.valid && passwordCtrl.touched" lines="none">
            <ion-label color="danger">パスワードは6文字以上</ion-label>
          </ion-item>
        
          <ion-button class="submitBtn" [disabled]="!f.form.valid" (click)="userLogin()" seize="large" expand="block">
            ログイン
          </ion-button>
        </form>
      </ion-col>
    </ion-row>
  
  <ion-row>
    <ion-col size="12" size-sm="8" offset-sm="2">
      <ion-button class="subLink" (click)="gotoSignup()" fill="clear">
        アカウント登録はこちらから
      </ion-button>
    </ion-col>
  </ion-row>
</ion-grid>
</ion-content>
```

##auth.page.ts

```typescript:auth.page.ts
import { AuthService } from './auth.service';
import { Router } from '@angular/router';
import { Component, OnInit } from '@angular/core';
import { ToastController } from '@ionic/angular';
import { AngularFireAuth } from '@angular/fire/auth';
import { LoadingController } from '@ionic/angular';

@Component({
  selector: 'app-auth',
  templateUrl: './auth.page.html',
  styleUrls: ['./auth.page.scss']
})
export class AuthPage implements OnInit {
  login: {
    email: string;
    password: string;
  } = {
    email: '',
    password: ''
  };

  constructor(
    private router: Router,
    private authService: AuthService,
    private toastCtrl: ToastController,
    private afAuth: AngularFireAuth,
    private loadingCtrl: LoadingController,
  ) {}

  ngOnInit() {}

  async userLogin() {
    const loading = await this.loadingCtrl.create({
      message: 'ログイン中です'
    });
    this.presentLoading(loading);
    this.authService.login(this.login.email, this.login.password)
    .then(() => {
      loading.dismiss();
    });
  }

  gotoSignup() {
    this.router.navigateByUrl('/signup');
  }

  async presentLoading(loading) {
    return await loading.present();
  }
}
```

#signupページを作る
```terminal
ionic g page signup
```
##signup.page.html
```html:signup.page.html
<ion-header>
  <ion-toolbar color="primary">
    <ion-title>アカウント登録</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>
  <ion-grid>
    <ion-row>
      <ion-col size="12" size-sm="8" offset-sm="2">
        <form #f="ngForm" class="signupForm">
          <ion-item class="formItem">
            <ion-label position="floating">
              <ion-icon name="mail"></ion-icon>メールアドレス
            </ion-label>
            <ion-input required email [(ngModel)]="signup.email" type="email" name="signup.email" #emailCtrl="ngModel">
            </ion-input>
          </ion-item>
          <ion-item *ngIf="!emailCtrl.valid && emailCtrl.touched" lines="none">
            <ion-label color="danger">メールアドレスを入力してね!</ion-label>
          </ion-item>
          <ion-item class="formItem">
            <ion-label position="floating">
              <ion-icon name="key"></ion-icon>パスワード
            </ion-label>
            <ion-input required [(ngModel)]="signup.password" type="password" name="signup.password" minlength="6"
              #passwordCtrl="ngModel">
            </ion-input>
          </ion-item>
          <ion-item *ngIf="!passwordCtrl.valid && passwordCtrl.touched" lines="none">
            <ion-label color="danger">パスワードは6文字以上</ion-label>
          </ion-item>
          <ion-item class="formItem">
            <ion-label position="floating">
              <ion-icon name="person"></ion-icon>名前
            </ion-label>
            <ion-input required [(ngModel)]="signup.name" type="text" name="signup.name"></ion-input>
          </ion-item>
        
          <ion-button class="submitBtn" [disabled]="!f.form.valid" (click)="signUp()" size="large" expand="block">
            アカウント登録
          </ion-button>
        </form>
      </ion-col>
    </ion-row>

  <ion-row>
    <ion-col size="12" size-sm="8" offset-sm="2">
      <p>アカウント登録を完了すると、
        <a href="">プライバシーポリシー</a>
        に同意した事になります。</p>
    </ion-col>
  </ion-row>

  <ion-row>
    <ion-col size="12" size-sm="8" offset-sm="2">
      <ion-button class="subLink" (click)="goLogin()" fill="clear">
        ログインはこちら
      </ion-button>
    </ion-col>
  </ion-row>
  </ion-grid>
</ion-content>
```

##signup.page.ts
```typescript:signup.page.ts
import { Router } from '@angular/router';
import { AuthService } from './../auth/auth.service';
import { Component, OnInit } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/auth';
import { ToastController, AlertController } from '@ionic/angular';

@Component({
  selector: 'app-signup',
  templateUrl: './signup.page.html',
  styleUrls: ['./signup.page.scss']
})
export class SignupPage implements OnInit {
  signup: {
    email: string;
    password: string;
    name: string;
  } = {
      email: '',
      password: '',
      name: ''
    };

  constructor(
    private router: Router,
    private afAuth: AngularFireAuth,
    private toastCtrl: ToastController,
    public authService: AuthService,
    private alertCtrl: AlertController
  ) { }

  ngOnInit() { }

  signUp() {
    this.afAuth.auth
      .createUserWithEmailAndPassword(this.signup.email, this.signup.password)
      .then(created => {
        const newUser = created.user;
        newUser.updateProfile({
          displayName: this.signup.name,
          photoURL: ''
        })
          .then(async () => {
            console.log('signup ' + this.signup.name);
            const toast = await this.toastCtrl.create({
              message: `${created.user.displayName}さんを登録しました`,
              duration: 3000
            });
            this.authService.updateUserData(created.user);
            this.router.navigateByUrl('/home');
            await toast.present();
          });
      })
      .catch(error => {
        console.log(error);
        const code = error.code;
        let message = 'エラーが発生しました。もう一度お試しください。。。';
        if (code === 'auth/email-already-in-use') {
          message = 'このメールアドレスは既に登録されています。。。';
        }
        this.showAlert(message);
      });
  }

  goLogin() {
    this.router.navigateByUrl('/auth');
  }

  private showAlert(message: string) {
    this.alertCtrl
      .create({
        header: 'ユーザー登録に失敗',
        message: message,
        buttons: ['OK']
      })
      .then(alertEl => alertEl.present());
  }
}
```


urlの最後を`/signup`にしてアクセスしてみましょう。
アカウント登録とログインがうまくいってるなら、Firebaseコンソールでもユーザーが登録されていることが確認できる。





#最後に
続き:point_down:
[その６ ログアウト処理とサイドバー](https://qiita.com/kokogento/items/cf024c80a9b50aeb91ed)


ログアウトがまだ一切できていないので、次回やりたい。

各ストアで配信されてます。
興味があれば使ってみてください。:relieved:

<a href="https://apps.apple.com/jp/app/%E6%80%A7%E6%B4%BB%E3%82%AB%E3%83%AC%E3%83%B3%E3%83%80%E3%83%BC/id1484151444?mt=8">Apple Store</a>

[Google Play](https://play.google.com/store/apps/details?id=com.hCalendar.app)