2019.02.04
Ionic 4のアプリケーションを作成して、AWS Amplifyを使ってバックエンドを構築した。ログイン周りのビューはAWS Amplifyで提供される<amplify-authenticator>
コンポーネントを利用して構築した。
Angularのモジュール機能を使って画面ごとにモジュールを分割したときに、複数画面で利用するコンポーネントや共通の状態を管理するためのモジュールとしてCoreModuleとSharedModuleを作った。
ログイン機能をもつWebアプリケーションやスマホアプリの場合、未ログイン状態でのみアクセスできる画面と、ログイン状態でのみアクセスできる画面と、どちらの状態でもアクセスできる画面の3種類がある。
ログイン状態の管理にAWS Amplifyを使ったとき、そのログイン状態に応じてIonic 4のアプリのルーティングが自動的に切り替わるようにする。
また、ログインが必須のページに未ログインでアクセスした場合はログインフォームにリダイレクトされる処理も実装する。
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
メソッドを使うことでアニメーションなしで画面遷移を行うようにした。
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
にアクセスすると/
へリダイレクトされるように実装できた。
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のログイン状態に応じて画面を遷移する実装と、意図しない画面へのアクセスを防ぐガードを実装した。また、AWS Amplifyで提供されるテーマを利用して見た目を整えた。
次回はいよいよバックエンドのAppSyncのスキーマを定義して、アプリとしての機能を形作っていきたい。 要件がわかりやすく、基本形がシンプルなTwitterを見本にして、Ionic 4とAppSyncを使ってトレースしていこうと思う。