admin guard: FireAuthを使って管理者ページを制限したいのだが

#Firebase ：FireAuthを使って管理者ページを制限したいのだが

###### 前々々回：[Angularの新規プロジェクト作成手順 - material2とService Workerは使いたい](https://qiita.com/atomyah/items/3e125b072dd74181e582)

###### 前々回：[AngularプロジェクトにFirebaseを導入設定](https://qiita.com/atomyah/items/750d076e3dd5449423fd)

###### 前回：[Angular + FirebaseでCRUD機能を実現](https://qiita.com/atomyah/items/e27c6d2c08f4ac260cfe)

<br>
Firebaseの認証機能、基本的にユーザー、ビジターを認証するための機能みたいで、.htaccessを使ったベーシック認証のような単純な使い方できないのだろうか。

どうもFirebase Functionを使えばベーシック認証機能を使えるらしいが、Authenticationでもっと簡単にできるはずだ。

試行錯誤してできた、の備忘録。


<br>
### Firebaseの設定

いきなり管理者ユーザーを追加してしまう。
![1556423690624.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/92346736-0076-109f-a32d-1f3390176259.png)


メール／パスワードのみ有効にする。
![1556423834375.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/aff53c97-0cf0-5c03-665b-97bf30a398b8.png)

<br>

そうしたら次にログイン画面を作成する。
loginコンポーネントを作成

```cmd
C:\ProjectA>ng g c component/login
```

そしてapp.route.tsにルート追加
初期画面がログイン画面になるようにする。
###### app.route.ts

```typescript
//====================
// ルータ定義
//====================

import {Routes} from "@angular/router";
import {RootComponent} from "./component/root/root.component";
import { HomeComponent } from './component/home/home.component';
import { LoginComponent} from './component/login/login.component';

//urlパスと表示するコンポーネントの関連づけ
export const AppRoutes: Routes = [
    {path: "", component: LoginComponent},　// 変更
    {path: "login", component: LoginComponent}, 　// 追加
    {path: "home", component: HomeComponent},　
];
```

<br>

で、login.component.htmlにフォーム画面を、、、の前に、FirebaseのAuthenticationサービスを作っておこう。



```cmd
C:\ProjectA>ng g s service/auth.service
```



###### auth.service.tsを開いて以下のように編集
**auth.service.ts**

```typescript
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { AngularFireAuth } from '@angular/fire/auth';
import { User } from 'firebase';


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

  constructor(
    private router: Router,
    private afAuth: AngularFireAuth,
  ) {
    this.afAuth.authState.subscribe(user => {
      if (user) {
        this.user = user;
        localStorage.setItem('user', JSON.stringify(this.user));
      } else {
        localStorage.setItem('user', null);
      }
    })
   }

   async login(email: string, password: string) {
    try {
        await  this.afAuth.auth.signInWithEmailAndPassword(email, password)
        this.router.navigate(['home']);
      } catch (e) {
        alert("Error!" + e.message);
      }
    }

    async logout(){
      await this.afAuth.auth.signOut();
      localStorage.removeItem('user');
      this.router.navigate(['/']);
    }

    get isLoggedIn(): boolean {
      const user = JSON.parse(localStorage.getItem('user'));
      return user !== null;
    }

     
}

```



認証ステートは`afAuth.authState.subscribe`だけで簡単に取得。ここではブラウザのlocalstrageにuser情報を格納。

あとは見ての通り。signinメソッドはない。（新しくユーザを追加する必要はないので）。

<br>

では、ログイン画面を。

###### login.component.html

```html
<div class="myContainerMain">
 <div style="yokonarabi">
     
  <mat-form-field>
    <input matInput type="email" placeholder="Email" #userEmail required autofocus>
  </mat-form-field>

  <mat-form-field>
    <input matInput placeholder="Password" #userPassword required>
  </mat-form-field>


  <mat-form-field>
    <button mat-flat-button color="accent" (click)="authService.login(userEmail.value, userPassword.value)">Login</button>
  </mat-form-field>
     
 </div>

</div>

```



画面はこう。
![1556428591991.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/c0d0da13-7210-5459-0a7d-46779f675d09.png)

ボタンのクリックでauth.serviceのloginメソッドを呼び出してるだけ。

`(click)="authService.login(userEmail.value, userPassword.value)"`

auth.serviceを呼び出せるように、login.component.tsでインポートしておく。

###### login.component.ts

```typescript
import { Component, OnInit } from '@angular/core';
import { AuthService } from  '../../service/auth.service'; // 追加

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['../../common.css','./login.component.css']
})
export class LoginComponent implements OnInit {

  constructor(private authService: AuthService) { }　　// 追加

  ngOnInit() {
  }

}

```



<br>

### ログアウトボタンを追加


ログインするとhome画面に飛ぶようになっている。

###### auth.service.ts

```typescript
   async login(email: string, password: string) {
    try {
        await  this.afAuth.auth.signInWithEmailAndPassword(email, password)
        this.router.navigate(['home']); // ← ココ
      } catch (e) {
        alert("Error!" + e.message);
      }
```



なのでhome.component.htmlにログアウトボタンを追加する。

###### home.component.html

```html
…上略
    <div class="yokonarabi">
      <button mat-flat-button color="accent" (click)="authService.logout()">Logout</button>
    </div>
```



authService.logout()を呼び出せるよう、home.component.tsにもauth.serviceをインポートしておく。

###### home.component.ts

```typescript
import { Component, OnInit } from '@angular/core';
import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore';
import { Comments } from '../../class/comments';  // commentsデータタイプインターフェース
import { Observable } from 'rxjs'; // 正式名称「Reactive Extensions for JavaScript」
import { FormBuilder,FormControl,FormGroup,Validators } from '@angular/forms';
import { map } from "rxjs/operators";
import { AuthService } from  '../../service/auth.service'; 　　// 追加

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['../../common.css','./home.component.css']
})
export class HomeComponent implements OnInit {


  commentsRef: AngularFirestoreCollection<Comments>;
  comments: Observable<Comments[]>;


  constructor(private db: AngularFirestore, private fb: FormBuilder,private authService: AuthService) {    　　// 追加

    this.commentsRef = this.db.collection<Comments>('comments') //, ref => ref.where('initial', '==', this.SearchByInitial))
    this.comments = this.commentsRef.snapshotChanges().pipe
      (map(actions => {
      return actions.map(action => {
        const data = action.payload.doc.data() as Comments;
        const id = action.payload.doc.id;
        return { id, ...data };
        });
      }));      
  }
…以下略…
```



<br>


### ログインしてみる

![1556429388801.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/470edf15-08e2-7a77-09c2-8a2cde290a06.png)


<br>
成功すればhome画面に飛んでログアウトボタンが表示される。
![1556429518439.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/5b252a16-f9b5-221a-48ed-e5e81f6dfaab.png)


<br>

ログアウトボタンをクリックすると、ルート画面、すなわちログイン画面に戻るようになっている。

###### auth.service.ts

```typescript
    async logout(){
      await this.afAuth.auth.signOut();
      localStorage.removeItem('user'); 
      this.router.navigate(['/']);　// ← ココ
    }
```
###### add.route.ts

```typescript
//urlパスと表示するコンポーネントの関連づけ
export const AppRoutes: Routes = [
    {path: "", component: LoginComponent}, 　// ← ココ
    {path: "login", component: LoginComponent},
    {path: "home", component: HomeComponent, canActivate: [AdminGuard]},
];
```

<br>

戻った。
![1556429976100.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/37726883-a071-0f62-82fa-189002b50f21.png)

<br>

しかし・・・・

今のままでは別にログインしなくとも、URLパスを知っていれば/homeに行けてしまう。

![1556430173261.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/1ea43403-fcae-1442-f723-e9890b61f846.png)

<br>

### Router Guardを利用する

それを防ぐために、guardというものを作成する。



```cmd
C:\projectA>ng g guard component/admin
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/bbb021e6-0713-3a54-50c9-976970dcb4d7.png)
選ぶオプションはこうしておく。詳細は調べて。

<br>



admin.guard.tsが生成されるので、以下のように編集する。


###### admin.guard.ts

```typescript
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';

import { AuthService } from './auth/auth.service'; // 追加

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

  constructor(private authService: AuthService){}

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
      return this.authService.isLoggedIn; // 変更
  }
}
```



canActivate()メソッドが、authServiceのisLoggedInメソッドを呼び出せるようにしただけ。

<br>
そうしたら、app.route.tsを開いて、ガードしたいURLに`canActivate: [AdminGuard]`を追加する。

###### app.route.ts

```typescript
//====================
// ルータ定義
//====================

import {Routes} from "@angular/router";
import {RootComponent} from "./component/root/root.component";
import { HomeComponent } from './component/home/home.component';
import { LoginComponent} from './component/login/login.component';
import { AdminGuard } from  './component/admin.guard';         // 追加

//urlパスと表示するコンポーネントの関連づけ
export const AppRoutes: Routes = [
    {path: "", component: LoginComponent},
    {path: "login", component: LoginComponent},
    {path: "home", component: HomeComponent, canActivate: [AdminGuard]},  // 変更
];

```



これでOK!

<br>

### UrlTreeを導入

あとで知ったが、Angular6まではguardはbooleanしか返してくれなかった（つまりログインしてないとなにも表示されない）。でも今はUrlTreeという変数を返してくれる。その中にはRouteステートが入っている。



なのでadmin.guard.tsを編集してログイン失敗時に任意のパスへリダイレクトするようにできる。

###### admin.guard.ts

```typescript
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, Router, RouterStateSnapshot, UrlTree } from '@angular/router';  // 変更 RouterとUrlTreeを追加した
import { Observable } from 'rxjs';
import { AuthService } from  '../service/auth.service';

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

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

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean | UrlTree {　　　　　　　　　// 変更　UrlTreeを追加した
      if(this.authService.isLoggedIn){
        return true;
      }
      else{
        return this.router.parseUrl("login");　　// 追加
      }
  }
}

```



authService.isLoggedInがFalseなら、ログインページへ飛ぶ。

直接/homeをURLにタイプしてみてもやはりログインページへ飛ぶ

<br>

### Firestoreのルールを変更

いちばん大切で複雑な記述。

詳細はのちのち学ぼう。

とりあえず…

<br>

Authenticationに行って、追加した自分のアカウントのUIDをコピー
![1556432938080.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/ac8276d0-d8a9-bf76-14c5-e659da1a1bbd.png)


Databaseのルールへ行って、こんな感じで。
![1556434064314.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/62931a72-1c5b-b2e8-ab42-7229b9438875.png)


commentsコレクションの、readはすべて許し、writeは管理人UIDのみ許す、という設定。

/comments/{comment} でcommentsコレクションのすべて、というワイルドカード


以下の設定だと「誰でも読める、認証された者は誰でも書ける、更新と削除は管理者だけ、という設定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/f7807504-285d-d9bb-072a-abd9b0245793.png)


<br>

以上。

Githubはこちら。

<https://github.com/atomyah/ProjectA>

<br>