Designing and Engineering "遊び心"駆動開発

Playful IT
2/4 6:52

AWS Amplifyのログイン状態に応じてIonic 4の画面を切り替える

前回まで

Ionic 4のアプリケーションを作成して、AWS Amplifyを使ってバックエンドを構築した。ログイン周りのビューはAWS Amplifyで提供される<amplify-authenticator>コンポーネントを利用して構築した。

Angularのモジュール機能を使って画面ごとにモジュールを分割したときに、複数画面で利用するコンポーネントや共通の状態を管理するためのモジュールとしてCoreModuleとSharedModuleを作った。

今回やること

ログイン機能をもつWebアプリケーションやスマホアプリの場合、未ログイン状態でのみアクセスできる画面と、ログイン状態でのみアクセスできる画面と、どちらの状態でもアクセスできる画面の3種類がある。

ログイン状態の管理にAWS Amplifyを使ったとき、そのログイン状態に応じてIonic 4のアプリのルーティングが自動的に切り替わるようにする。

また、ログインが必須のページに未ログインでアクセスした場合はログインフォームにリダイレクトされる処理も実装する。

ログイン状態を扱うAuthServiceを作成する

AWS Amplifyが提供するAmplifyServiceをDIすることで、Angularのサービスやコンポーネントから利用することができる。

ログイン状態を取得するメソッドは見つからなかったが、未ログイン状態amplifyService.auth().currentSession()を実行するとエラーが発生することがわかった。

それを踏まえて、ログイン状態をPromiseで返すメソッドを含むAuthServiceを定義した。



import { Injectable } from '@angular/core';
import { AmplifyService } from 'aws-amplify-angular';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export enum AuthState {
  SignedOut           = 'signedOut',
  SignedIn            = 'signedIn',
  MfaRequired         = 'mfaRequired',
  NewPasswordRequired = 'newPasswordRequired',
}

@Injectable({
  providedIn: 'root'
})
export class AuthService {

  constructor(
    private amplifyService: AmplifyService,
  ) { }

  /**
   * ログイン中か判定
   * @return true/ログイン中
   */
  async isSignedIn(): Promise<boolean> {
    try {
      await this.amplifyService.auth().currentSession();
    } catch {
      return Promise.resolve(false);
    }
    return Promise.resolve(true);
  }

  /**
   * ログイン状況
   */
  get authState$(): Observable<AuthState> {
    return this.amplifyService.authStateChange$.pipe(
      map(state => <AuthState>state.state),
    );
  }

  /**
   * ログアウト
   */
  signOut() {
    this.amplifyService.auth().signOut();
  }
}

このサービス自体は状態を持たないものの、状態を持つAmplifyServiceと密結合なのでCoreModuleに登録してみた。

ログインしたときにメイン画面に遷移する処理を実装する

ログイン画面であるAuthPageのngOnInitでAuthServiceを監視して、ログイン状態に切り替わったら/に移動するように実装した。



import { Component, OnDestroy, OnInit } from '@angular/core';
import { AuthService, AuthState } from '../core/services/auth.service';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { NavController } from '@ionic/angular';

@Component({
  selector: 'app-auth',
  templateUrl: './auth.page.html',
  styleUrls: ['./auth.page.scss'],
})
export class AuthPage implements OnInit, OnDestroy {

  private onDestroy = new Subject();

  constructor(
    private navCtrl: NavController,
    private authService: AuthService,
  ) { }

  ngOnInit() {
    this.authService.authState$.pipe(
      takeUntil(this.onDestroy),
      filter(state => state === AuthState.SignedIn),
    ).subscribe(() => {
      this.navCtrl.navigateRoot('/');
    });
  }

  ngOnDestroy() {
    this.onDestroy.next();
  }
}

ログアウト時にログイン画面に遷移する処理を実装する

同様に、ログアウトしたときにログイン画面に自動的に移動する処理を書く。



<ion-header>
  <ion-toolbar>
    <ion-title>
      Ionic Amplify
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>
  <h1>home.page.html</h1>
  <ion-button (click)="onSignOutClicked()" expand="block">ログアウト</ion-button>
</ion-content>



import { Component, OnDestroy, OnInit } from '@angular/core';
import { NavController } from '@ionic/angular';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { AuthService, AuthState } from '../core/services/auth.service';

@Component({
  selector: 'app-home',
  templateUrl: './home.page.html',
  styleUrls: ['./home.page.scss'],
})
export class HomePage implements OnInit, OnDestroy {

  private onDestroy = new Subject();

  constructor(
    private navCtrl: NavController,
    private authService: AuthService,
  ) { }

  ngOnInit() {
    this.authService.authState$.pipe(
      takeUntil(this.onDestroy),
      filter(state => state === AuthState.SignedOut),
    ).subscribe(() => {
      this.navCtrl.navigateRoot('/auth');
    });
  }

  ngOnDestroy() {
    this.onDestroy.next();
  }

  onSignOutClicked() {
    this.authService.signOut();
  }
}

画面遷移には@ionic/angularから提供されているNavControllerを使った。navigateRootメソッドを使うことでアニメーションなしで画面遷移を行うようにした。

意図しない画面へのアクセスを防ぐためにAngular Routerのガード機能を使う

Ionic 4では各画面にURLが存在するので、URL直打ちでアクセスされてしまったときに意図しない画面が表示されるのは困る。Angular Routerのガード機能を使ってこれを防ぐ。

未ログイン状態でしかアクセスできないページにはUnauthGuardを、ログイン状態でしかアクセスできないページにはAuthGuardを設定する。



import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from '../services/auth.service';

@Injectable({
  providedIn: 'root'
})
export class UnauthGuard implements CanActivate {

  constructor(
    private router: Router,
    private authService: AuthService,
  ) {}

  canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    return this.authService.isSignedIn().then(isSignedIn => {

      if (isSignedIn) {
        this.router.navigate(['/']);
      }

      return !isSignedIn;
    });
  }

}



import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from '../services/auth.service';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {

  constructor(
    private router: Router,
    private authService: AuthService,
  ) {}

  canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    return this.authService.isSignedIn().then(isSignedIn => {

      if (!isSignedIn) {
        this.router.navigate(['/auth']);
      }

      return isSignedIn;
    });
  }

}

これらのガードがルーターから読み込まれるように設定する。



import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AuthGuard } from './core/guards/auth.guard';
import { UnauthGuard } from './core/guards/unauth.guard';

@NgModule({
  imports: [
    RouterModule.forRoot([
      { path: '',     loadChildren: './home/home.module#HomePageModule', pathMatch: 'full', canActivate: [ AuthGuard ] },
      { path: 'auth', loadChildren: './auth/auth.module#AuthPageModule', canActivate: [ UnauthGuard ] },
    ]),
  ],
  exports: [RouterModule]
})
export class AppRoutingModule { }

これで、未ログイン状態で/にアクセスすると/authへ、ログイン状態で/authにアクセスすると/へリダイレクトされるように実装できた。

ついでにAWS Amplifyで提供されているテーマを適用する

src/global.scssにAWS Amplifyのテーマをインポートする行を追加することで、Ionicのボタン等のコンポーネントのスタイルをAmplify風に変更することができる。



// http://ionicframework.com/docs/theming/
@import '~@ionic/angular/css/core.css';
@import '~@ionic/angular/css/normalize.css';
@import '~@ionic/angular/css/structure.css';
@import '~@ionic/angular/css/typography.css';

@import '~@ionic/angular/css/padding.css';
@import '~@ionic/angular/css/float-elements.css';
@import '~@ionic/angular/css/text-alignment.css';
@import '~@ionic/angular/css/text-transformation.css';
@import '~@ionic/angular/css/flex-utils.css';

@import '~aws-amplify-angular/theme.css'; // この行を追加

AWS Amplifyのログイン状態に応じてIonic 4の画面を切り替える

まとめ

今回はAWS Amplifyのログイン状態に応じて画面を遷移する実装と、意図しない画面へのアクセスを防ぐガードを実装した。また、AWS Amplifyで提供されるテーマを利用して見た目を整えた。

次回はいよいよバックエンドのAppSyncのスキーマを定義して、アプリとしての機能を形作っていきたい。 要件がわかりやすく、基本形がシンプルなTwitterを見本にして、Ionic 4とAppSyncを使ってトレースしていこうと思う。

関連記事
2/5 6:44

サーバーレスを学ぶのにAWS Amplifyを使うのをやめた件

AWS Amplifyとの出会い 目黒にあるAWS Loft Tokyoにて行われたBlack Beltセミナーの公開収録に参加して初めてAmplifyを知った。 AWS BlackBeltセミナー Amplify@AWS Loft 2018/11/6 それまでAWS…

2/4 10:59

AWS AmplifyのAPI.graphqlではなくAWSAppSyncClientでAppSyncにアクセスしてみる

AWS Amplifyが提供するGraphQL AWS Amplifyを使ってAppSyncを利用する場合、 import { AmplifyService } from 'aws-amplify-angular'; で読み込んだAmplifyServiceをDI…

2/4 7:32

AWS AmplifyでAppSyncのGraphQLスキーマを作成する

目指すもの まずはログインしているユーザーがつぶやきを投稿できるだけのシンプルなアプリケーションを目指す。Amazon Cognito上ではユーザーは複数作成されるが、つぶやきにはユーザーは関連付けない。 学びたいこと AWS Amplifyで定義したGraphQL…

2/2 11:08

AWS AmplifyをIonic 4で使うときのモジュール分割を考える

前回まで AWS AmplifyをIonic 4で使ってみる Ionic 4(Angular 7)のアプリケーションを作成して、AWS AmplifyでAWS上にAWS AppSyncとAmazon Cognitoを使ったAPI…

2/1 9:52

AWS AmplifyをIonic 4で使ってみる

あらまし AWSのマネージドサービスをうまく活用することで、高い可用性を持ったアプリケーションを開発できる。2019年はスマホアプリ・PWAのバックエンドにAWS…

プロフィール