FirebaseとNuxt.jsを使ってユーザ認証関係を簡単に作ってみる+1ヶ月前の自分に教えたいリンク集

この記事は、[Nuxt.js #2 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/nuxt-js-2)の12日目の記事です。

# 概要
この記事ではNuxt.jsでプロジェクトの作成からユーザ認証関係(ログインしないと見れないページの作成方法)を紹介します。

具体的には認証を行うコンポーネントやプラグインを作成し、認証まわりの処理をおこないます。コンポーネントで作成するため再利用性は高い(はず)です。ご利用ください。

※Firebase, Nuxt, Vue初心者が書いた記事です。もっといい書き方とかあれば教えてください！

# 前提

`node`は10.14.1, `npm`は6.4.1を使っていきます。

```bash
$ node -v
v10.14.1
$ npm -v
6.4.1
```

# 1. プロジェクトの作成

[Nuxt.js x Firebase事始め - the industrial](https://omiend.hatenablog.jp/entry/2018/12/01/121105)を見ると、Nuxt.jsのプロジェクトの作成からFirebaseへのデプロイができます。

とりあえず、この記事だけで完結できるように必要なコマンドを説明します。

まず, Nuxtのプロジェクトを作成します。
ここらへんは以下の公式のリファレンスと一緒です。
[インストール - Nuxt.js](https://ja.nuxtjs.org/guide/installation)

```bash
npx create-nuxt-app <project-name>
```
途中でいろいろ聞かれますが、基本エンター連打で大丈夫です。(お好きに選んでください)

```bash
cd <project-name>
npm run dev
```

をして、[http://localhost:3000](http://localhost:3000)でページが見れます。


# 2. Firebaseのプラグインの作成

Firebaseをプロジェクトに追加します。package.jsonに追加したいので`--save`オプションを忘れずに。

```bash
npm install firebase --save
```

次にプロジェクトのpluginsの中にfirebase.jsを作成し、以下のように自分のFirebaseのプロジェクトのキーを記入します。

```~/plugins/firebase.js
import firebase from 'firebase'

if (!firebase.apps.length) {
  firebase.initializeApp({
    apiKey: "API_KEY",
    authDomain: "APP_ID.firebaseapp.com",
    databaseURL: "https://PROJECT_ID.firebaseio.com",
    projectId: "PROJECT_ID",
    storageBucket: "PROJECT_ID.appspot.com",
    messagingSenderId: "1234567890"
  })
}

export default firebase
```

キーの見方はFirebaseの自分のプロジェクトにいき、歯車アイコンをクリックすると下部に以下の項目があります。このウェブアプリにFirebaseを追加をクリックし、

<img width="965" alt="スクリーンショット 2018-12-13 10.27.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/91558/cce59567-bb6b-5d14-7c11-2f62147857f3.png">

表示される以下の`config`を上記のコードの`firebase.initializeApp({...})`に記入してください。

<img width="829" alt="スクリーンショット 2018-12-13 10.29.39.png" src="https://qiita-image-store.s3.amazonaws.com/0/91558/e506b56f-82d9-e15e-12cb-626ec9b239e3.png">

これで自分のプロジェクトと紐付けができました。
これ以降このプロジェクトでFirebase関係のAPIを用いる時は`import firebase from '~/plugins/firebase'`をして使います。するとキーが入った`firebase`が使えるようになります。

Firebaseのキーをコードにそのまま書いてもいいのかという問題点については以下の記事をご覧ください。結論から言うとDB関係はセキュリティルールで守るため**キーをコードに書いても大丈夫**です。もちろんdotenvとか使ってもいいかもしれません。
[javascript - Is it safe to expose Firebase apiKey to the public? - Stack Overflow](https://stackoverflow.com/questions/37482366/is-it-safe-to-expose-firebase-apikey-to-the-public)

また、プラグインの作成は以下の記事を参考にさせていただきました。
[Nuxt + Firebase Authentication | shimar's blog](https://blog.shimar.me/2018/03/31/nuxt-firebase-authentication.html)

# 3. ユーザデータの保存の準備
ログインしたあとにユーザ認証データをユーザの端末に保存するためvuexを使います。

`~/store/index.js`に以下のようにかきます。

```~/store/index.js
export const strict = false

export const state = () => ({
  user: null,
})

export const mutations = {
  setUser (state, payload) {
    state.user = payload
  }
}

export const actions = {
  setUser ({ commit }, payload) {
    commit('setUser', payload)
  }
}

export const getters = {
  isAuthenticated (state) {
    return !!state.user
  }
}
```

Firebaseで認証を行うのですが、行なったあとにこの`setUser`を用いて認証したデータを保存しておきます。認証が必要な各ページではこの`user`データを読み込みをログイン済みか検証します。

またVuexはロードするとデータが消えてしまうので、消さないようにユーザのlocalStorageを用いていい感じに保存できるようにしておきます。そのために`vuex-persistedstate`を使います。

以下の記事を参考に`npm install vuex-persistedstate --save`、`persistedstate.js`の作成、`nuxt.config.js`内の設定変更を行なってください。良記事感謝感激謝謝茄子
[nuxt.jsを使う時にlocalStorageでstoreを永続化する - Qiita](https://qiita.com/sakapun/items/a0cf5698751ae70c8088)

これでロードしてもVuexのデータが消えなくなりました！
ただひとつ難点があり、createdやmounted時にVuexの値が取得できません。以下の記事を参考
[あーありがち - Vuex PersistedStateの値の復帰のタイミングが謎だったのでNuxtのplugin書いて解決した](http://aligach.net/diary/20180608.html)

そのため、Vuexの値をページロード時に最初に取得し、なにかする場合には以下のように書く必要があります。(他の書き方もあります)

```javascript
import { mapState } from 'vuex'

export default {
  computed: {
    ...mapState(['user'])
  },
  mounted() {
    console.log(this.user) // ここだと取得できない
    setTimeout(() => {
      console.log(this.user) // ここだと取得できる
      // なにかしらの処理
    })
  }
}
```

# 4. 認証画面、認証コンポーネントの作成
まずログイン画面(/account)のページを作成します。
ログインはメールアドレスとパスワードでログインすることを想定しています。

```~/pages/account.vue
<template>
  <div>
    <!-- ログイン中に表示される画面 -->
    <div v-if="isAuthenticated">
      {{ user.email }}でログイン中です<br>
      <button v-on:click="logout">ログアウト</button><br>
      <a href="/member-page">メンバーページへ</a>
    </div>
    <!-- ログインしていない時に表示される画面 -->
    <div v-else>
      メール<br>
      <input type="text" v-model="email"><br>
      パスワード<br>
      <input type="password" v-model="password"><br>
      <button v-on:click="login">ログイン</button>
    </div>
  </div>
</template>

<script>
import firebase from '~/plugins/firebase'
import { mapActions, mapState, mapGetters } from 'vuex'
export default {
  data() {
    return {
      email: '',
      password: ''
    }
  },
  computed: {
    ...mapState(['user']),
    ...mapGetters(['isAuthenticated'])
  },
  mounted() {
    firebase.auth().onAuthStateChanged((user) => {
      this.setUser(user)
    })
  },
  methods : {
    ...mapActions(['setUser']),
    login() {
      firebase.auth().signInWithEmailAndPassword(this.email, this.password)
      .then(user => {
        // ログインしたら飛ぶページを指定
        // this.$router.push("/member-page")
      }).catch((error) => {
        alert(error)
      });
    },
    logout() {
      firebase.auth().signOut()
      .then(() => {
        this.setUser(null)
      }).catch((error) => {
        alert(error)
      })
    }
  }
}
</script>
```

実際に作成したaccountページの[http://localhost:3000/account](http://localhost:3000/account)に飛ぶと以下のような感じ

<img width="837" alt="スクリーンショット 2018-12-13 13.18.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/91558/a26f9248-14e5-3706-03c3-8cd385f04290.png">

おっと、まだアカウントを作成していませんでしたね!(海外のライブラリのチュートリアルありがちな唐突な気さくさ)

Firebaseの管理画面でAuthenticationの項目のログイン方法の`メール/パスワード`を有効にしておく必要があります。ここで、Firebaseの管理画面のAuthenticationの項目のユーザから適当にユーザを追加しておいてください。

<img width="1024" alt="スクリーンショット 2018-12-13 11.24.34.png" src="https://qiita-image-store.s3.amazonaws.com/0/91558/bea00f16-0432-adbb-0135-11d9abb68bc6.png">

<img width="993" alt="スクリーンショット 2018-12-13 11.25.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/91558/befeeb2d-4ed8-bc2a-6498-ff463932113f.png">

これでログインできるようになりました。
実際に作成したアカウントでログインをしてみると以下のような感じです。

<img width="837" alt="スクリーンショット 2018-12-13 13.19.36.png" src="https://qiita-image-store.s3.amazonaws.com/0/91558/58ec5116-3a6b-0a14-73cc-0d33d0a173c7.png">

実際にサービスにする際には新規アカウント作成画面を作ったりしてあげる必要があります。（余裕があれば書きたい）

また、GoogleやTwitter,Facebookアカウントを利用したログインとかもFirebaseで**めっちゃ簡単に実装できます。**スマホユーザのことを考えると該当のログイン画面にリダイレクトしてあげて本サービスにまたリダイレクトしてあげる必要があります。このリダイレクトがめんどくて、サービスのルートにリダイレクトされるので、そこらへんをいい感じに書いてあげる必要があります。(余裕があれば書きたい）

# 5. 認証が必要な画面を簡単に作成

まず認証をしていないと見れないコンポーネント(認証していないとログイン画面に飛ばされるコンポーネント)を作成します。
`components`に`members-only.vue`を作成して以下のコードを書きます。

```~/components/members-only.vue
<template>
  <div v-if="loaded">
    <slot/>
  </div>
</template>

<script>
import { mapGetters, mapState } from 'vuex'

export default {
  data() {
    return {
      loaded: false
    }
  },
  computed: {
    ...mapGetters(['isAuthenticated'])
  },
  async mounted() {
    setTimeout(() => {
      if (!this.isAuthenticated) {
        // ログインしていなかったら飛ぶページを設定
        this.$router.push('account')
      }
      this.loaded = true
    }, 0)
  }
}
</script>
```

以上のコンポーネントを用いて、実際にログインしていないと見れないページを以下のように作成します。

```~/pages/member-page.vue
<template>
  <members-only>
    <h1>ログインしていないと見れないページ</h1>
    <a href="/account">アカウントページへ</a>
  </members-only>
</template>

<script>
import MembersOnly from '~/components/members-only.vue'

export default {
  components: {
    MembersOnly
  }
}
</script>
```

実際に作成したmember-pageページの[http://localhost:3000/member-page](http://localhost:3000/member-page)に飛ぶと以下のような感じ

ログインしているとちゃんと表示されます。ログインしていないとaccountページに飛ばされます。

<img width="837" alt="スクリーンショット 2018-12-13 13.20.52.png" src="https://qiita-image-store.s3.amazonaws.com/0/91558/46341ec4-5457-cdf9-b1aa-b17c6d1030fe.png">

`members-only`コンポーネントを作成したことによって、ログインしていないと見れないページを簡単に作成できました。コンポーネントをロードして、該当のページの親要素にしてあげるだけです。

ちなみにユーザ側でVuexの値はいじることができます。そのため実際はログインしていなくても、ログインしているように見せかけて`members-only`コンポーネントをつけた内部をみることができます。
FirebaseではDBにセキュリティルールをつけて、ユーザごとの読み書き制限ができます。そのため、以上のような場合でもきちんとデータ側にセキュリティルールをつけておけばいいということになります。つまり悪意のあるユーザが自分のブラウザ上でVuexの値をいじっても、セキュリティルールで守られたデータは取得できないので、大丈夫ということになります。

おわり。

# 番外編 1ヶ月前の自分に教えたいリンク集

FirebaseのDBやってると「は？リレーション組めないやんけ💩」ってなると思います。それはSQLマンです。Firebaseでは非正規化が正規化です。ってのを教えてくれるFirebaseの公式のYouTubeの動画がコレです。↓
言語は英語ですが、ただしい日本語の字幕がついてるので問題ないです。
[SQL Databases and the Firebase Database - The Firebase Database For SQL Developers #1 - YouTube](https://www.youtube.com/watch?v=WacqhiI-g_o&list=PLl-K7zZEsYLlP-k-RKFa7RyNPa9_wCH2s)

NoSQLはわかった、でもこれってどうやって読み書き制限すればええんか？ってなった時に使えるのが↓
先ほども触れましたが、FirebaseはAPIキーはアプリケーションを特定するためだけに使い読み書き制限のために用いません。
FirebaseではDBの読み書き制限はセキュリティルールを記述することによって行います。
[Firebase Realtime Database ルールについて  |  Firebase Realtime Database  |  Firebase](https://firebase.google.com/docs/database/security/?hl=ja)
[firebase realtime database でよく使う rule - Qiita](https://qiita.com/mironal/items/b93742e8ef72b3268beb)

FirebaseのDBのクエリの公式リファレンスのサンプル少なくない?ってなった時に使えるのが↓です。
公式リファレンスはわかりやすくするため(?)、必要最低限のことが書いてあります。そのためこれをこうしたいっていうのはググるといいスニペットが出てきます。
[SQLをFirebaseで書き換えるためのチート集 - Qiita](https://qiita.com/makotoiwkb/items/9221d7e0829cc06fa63d)