Nuxt.js + Vuex + Firebase(Authentication, Cloud Firestore)でTwitterユーザー向けの伝言板を作る

この記事は「[株式会社オープンストリーム Advent Calendar 2019](https://qiita.com/advent-calendar/2019/opst)」の19日目の記事です。

どうも、最近はFirebaseに触れていない @ysd_marrrr です。
今回はFirebaseのAuthenticationとFirestoreでユーザーを認証して、そのユーザーでやり取りするサンプルを作りました。
フロントエンドはNuxtを使っているのですが、Firebaseとどう連携させるか悩みながら決めたので共有します。

## 作るサンプルについて

「Twitterのタイムラインでは遥か彼方に流れてしまう、だけども自分のプロフィールにピン留めしておくものでもない情報をみんなでシェアする」伝言板を作ります。

 * Twitterにログインしなくとも見ることはできます
 * Twitterでログインすると書き込めるようになります
 * 自分の書きこんだ投稿「だけ」削除できるものとします

![sample-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/199d466a-5b02-31b0-4b2a-318caa0af5c2.png)
![sample-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/a05f7b3d-e4a3-2746-3ac9-090c19d19c70.png)

ソースコードはこちらになります。
https://github.com/ysd-marrrr/nuxt-firebase-board

## 開発環境

macOSと

```bash
$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.15.5
BuildVersion:	19F101

$ node -v
v14.5.0

$ npm -v
6.14.5

$ yarn -v
1.22.5

$ firebase -V
8.10.0
```

Windowsで動作確認をしています。


```bash
C:\Windows>ver

Microsoft Windows [Version 10.0.18363.476]

...

$ node -v                       
v13.2.0                         
                                
$ npm -v                        
6.13.1                          
                                
$ yarn -v                       
1.21.0                          
```

## プロジェクトの作成

`create-nuxt-app` でプロジェクトを作ります。FirebaseのHostingを使うため"Choose custom server framework"は `None` にします(ローカルでフロントエンドの確認はできます！)

```bash
$ npx create-nuxt-app nuxt-firebase-board                                                                               
create-nuxt-app v2.12.0
✨  Generating Nuxt.js project in nuxt-firebase-board
? Project name nuxt-firebase-board
? Project description A board sample with Nuxt.js + Firebase(Auth, Cloud Firestore)
? Author name ysd-marrrr
? Choose the package manager Yarn
? Choose UI framework Bulma
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules Axios, Progressive Web App (PWA) Support, DotEnv
? Choose linting tools ESLint, Prettier, Lint staged files
? Choose test framework None
? Choose rendering mode Single Page App
? Choose development tools jsconfig.json (Recommended for VS Code)
```


## UIの作成

Nuxt + Firebaseの組み合わせが本記事のテーマのため、ここでは省略します。サンプルコードを見てみてください🙄
Atomic Design **もどきで** 進めています ~~気力あったらUIパーツを分けます~~

https://github.com/ysd-marrrr/nuxt-firebase-board/tree/master/layouts
https://github.com/ysd-marrrr/nuxt-firebase-board/tree/master/pages
https://github.com/ysd-marrrr/nuxt-firebase-board/tree/master/components


## Firebaseの導入

[Firebaseのコンソール](https://console.firebase.google.com/)を開き、プロジェクトを追加します。Google アナリティクスの利用は任意です。
Firebaseのプロジェクトを作り終わると「開始するにはアプリを追加してください」と表示されるので、3番目の「ウェブ」をクリックします。
![firebase-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/aff77b57-8e5d-86d1-f3e4-c11f99d4d16c.png)


再び新規作成する名前を入力しますが、 **プロジェクトの中に複数のアプリを追加できる仕組みのため** わかりやすい名前にしましょう。 
![firebase-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/ef12fea5-79ca-26f3-0fb2-8f7b75ea73ea.png)

 * **「Firebase Hostingを設定します」にチェックを入れます**
 * 次の「Firebase SDK の追加」はHTMLに直接挿入するためのオプションで、npmのパッケージからFirebaseを利用するため **何もせず次に進めます**

開発環境からFirebaseのプロジェクトを操作する `firebase-tools` を導入します。

```bash
npm install -g firebase-tools
```

次に、 `create-nuxt-app` で作成したプロジェクトのルートディレクトリに移動して、Firebaseのプロジェクトを設定します。
ログインの画面が出たらFirebaseのプロジェクトを作ったGoogle アカウントでログインします。

`firebase login` 時の "? Allow Firebase to collect CLI usage and error reporting information?" は `firebase-tools` を使っているときのエラー報告を送信するかどうかなので各自で Y/n を選んでください。

```bash
firebase login 
firebase init
```

`firebase init` の設定は次の通りにしました。 **Spaceキーで複数選択してEnterキーで確定する項目** があるため「英語だから」と逃げずに質問文をよく読みましょう。

```bash
? Are you ready to proceed? Yes
? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices. Hosting: Configure and deploy Firebase Hosting sites

=== Project Setup
? Please select an option: Use an existing project
? Select a default Firebase project for this directory: moments-sub (moments-sub)
i  Using project nuxt-board-sample (nuxt-board-sample)

=== Hosting Setup
? What do you want to use as your public directory? public
? Configure as a single-page app (rewrite all urls to /index.html)? Yes
```

見づらいですが、序盤の「使用するサービス」を選択する質問で

 * Hosting: Configure and deploy Firebase Hosting sites

を選択しています。
ここでFirestoreも選ぶと、のちに`firebase deploy`するときにHostingとFirestoreが同時にデプロイされてしまいます。慣れないうちはFirestoreを後で設定しましょう！

`./public/index.html` を生成したと表示されますが、Nuxtでビルドしたものを使うため無視します。


## Firebase Hostingを試す

プロジェクト設定時にHostingも設定しましたが、正しく設定されたかどうかNuxtでビルドしたものをHostingで確認してみましょう。

`firebase init` のHostingの設定を変えてもよいのですが、Nuxtのビルド先を変更します。`./nuxt.config.js` の `generate.dir` を追加します。
`buildDir` とこんがらがりますが、NuxtをSPAモードにしているため`generate.dir`の設定が適用されます。

```js:./nuxt.config.js
...
  },
  // 一番最後の"builds"の次に挿入する
  generate: {
    dir: 'public'
  }
```

そして、ビルドした後にHostingにデプロイします。コンソールでデプロイされたサイトのURLが表示されるので確認してみてください！

```bash
yarn build
firebase deploy
```

確認が終わったらHostingを無効化します。大丈夫です、次回の`firebase deploy`時に復活します。
https://firebase.google.com/docs/cli?hl=ja#hosting-commands

```bash
firebase hosting:disable
```


## Authentication の設定

Twitterアカウントでログインする…部分はFirebaseのAuthenticationで実装しましょう。

### Twitterのアプリ登録

Firebase AuthenticationでTwitterのアカウント🐦を使って認証できるようにしましょう。
Twitterのアカウントを使った認証は、 **Twitter側で開発者登録をして、その上で新しくアプリを作る必要があります。**
登録方法は次の記事にまとまってますが、英語で開発者アカウント・アプリの用途を説明する必要があるため ~~面倒くさいですね~~ **そこは頑張って登録を進めましょう。**

Twitter Developer 登録からアプリケーション作成まで〜 - Qiita
https://qiita.com/kei2ro/items/17fac4502119e9918763

アプリの作成ができると [Details] でアプリの詳細が開けるはずなので、そこの Keys and tokensでAPIキーを表示します。
使うAPIキーは**Consumer API keys**の方です。 Access token & access token secretは使いません。

![twitter-key-and-tokens.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/146bbf16-b9f0-942f-c326-0901e52dab50.png)

2020/1/20からAPIキーの表示はアプリ作成時の一度きりになるそうです。アプリ作成時にAPIキーをメモできなかった場合はもう一度APIキーを生成(Regenerate)する必要があります。
https://twittercommunity.com/t/upcoming-changes-to-access-token-and-secret-management/130851

#### Firebase Authenticationの認証方法設定

そして、Firebase Authenticationのコンソールを開いて「ログイン方法を指定」をクリックします。
![firebase-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/0f1b1ea5-7834-a8e6-054a-d01678e6158e.png)

「ログインプロバイダ」としてログインに使える認証方法がたくさんありますが、その中の [Twitter] をクリックします。
設定が開くので、「有効にする」をクリックした後Twitterのアプリの詳細から `APIキー` と `APIシークレット` をコピーしましょう。
![firebase-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/196ae8b3-138d-932d-6550-de6477c50ea6.png)


**ここで「保存」を押してしまいそうですが、** 「設定を完了するには、このコールバック URL を Twitter アプリの設定に追加します。」のURLをコピーします。

Twitterのアプリの詳細に戻り、App detailsから[Edit] -> [Edit details]をクリックして **Callback URLs** に貼り付けて保存しましょう。 
また、[Enable Sign in with Twitter] にチェックを入れましょう。

![twitter-app-detail.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/912d0c1b-0e54-8607-fed9-5cff38a379ca.png)

Callback URLsの設定が終わったら、Firebase Authenticationのコンソールに戻ってこちらも保存しておしまいです。



### Nuxtプロジェクト側にFirebaseを設定する

Nuxtプロジェクト側も設定します。先程Hostingは設定できましたが、NuxtのアプリからFirebase Authenticationを利用するにはnpmでFirebase SDKを導入する必要があります。

```bash
yarn add firebase
```

FirebaseのSDKが導入できたらFirebaseの接続情報を用意します。
コンソールでプロジェクトのトップを開き、作成したWebアプリの名前をクリックして歯車のアイコンをクリックします。
![firebase-5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/6a8f737a-bcc5-3b6a-4da6-2de5a299a435.png)

Webアプリの設定が開くので、 **一番下の Firebase SDK Snippet**までスクロールして「構成」をクリックします。こちらが接続情報です。
![firebase-6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/55501605-dba6-704f-a883-bba73bb0b325.png)

そのFirebaseの接続情報を `.env` ファイルに残します。`create-nuxt-app` で追加した dotenv(`./.env`) のファイルになります。
内容は次のように接続情報が並んでおり、 `=` の後に設定値を書きます。文字列を表す `""` は不要です。

```text:.env
FB_API_KEY=AIza...
FB_AUTH_DOMAIN=nuxt-board-sample.firebaseapp.com
FB_DATABASE_URL=https://nuxt-board-sample.firebaseio.com
FB_PROJECTID=nuxt-board-sample
FB_STORAGE_BUCKET=nuxt-board-sample.appspot.com
FB_MESSAGING_SENDER_ID=354814904070
FB_APPID=1:354814904070:web:d93e...
FB_MESUREMENTID=G-6CYVTEMH8Q
```

この設定はWeb上に公開されるため、`.gitignore` に登録して意図しない流出を防ぐ目的で `dotenv` を使用しません(`create-nuxt-app`で`dotenv`を使うと自動的に`.gitignore`に登録されます)
`dotenv`は別のFirebaseプロジェクト/環境でアプリを使いまわす際に便利そうですね。

次に、`./plugins/firebase-custom.js` を作成します。
NuxtでFirebaseを使えるうように初期化処理を追加しますが、 **`npm` で導入したFirebase SDKと混在するためplugins`のほうは名前を変えておきます。**

```js:./plugins/firebase-custom.js
import * as firebase from 'firebase/app'
import 'firebase/auth'
import 'firebase/firestore'

const firebaseConfig = {
  apiKey: process.env.FB_API_KEY,
  authDomain: process.env.FB_AUTH_DOMAIN,
  databaseURL: process.env.FB_DATABASE_URL,
  projectId: process.env.FB_PROJECTID,
  storageBucket: process.env.FB_STORAGE_BUCKET,
  messagingSenderId: process.env.FB_MESSAGING_SENDER_ID
  appId: process.env.FB_APPID,
  measurementId: process.env.FB_MEASUREMENTID
};

firebase.initializeApp(firebaseConfig)

const firestoreDb = firebase.firestore()
const dbSettings = { timestampsInSnapshots: true }
firestoreDb.settings(dbSettings)

// Firestoreの設定を適用するためfirebase.firestore()を読み替える
export { firestoreDb }
```


### Firebaseの接続情報について

Firebaseのアクセスキーは、Javascript SDKの場合 **公開される前提で設定します。** また、Firebase HostingではFirebaseの接続情報が公開されます。
そのため、誰でも(今作っているアプリをバイパスして)データベースであるCloud Firestoreにアクセスできる状態になります。

この時に **不正な操作からFirestoreのデータ(ドキュメント)を守るにはFirestoreのセキュリティルールのみが頼りになります。**
こちらの記事で接続情報の公開について詳しく解説しています。

Firebase apiKey ってさらしていいの? ほんとに? - Qiita
https://qiita.com/hoshymo/items/e9c14ed157200b36eaa5

[WIP] Firebase Cloud Firestore 接続情報が漏れるとパケ死しないか気になっていたので調べてるメモ - かもメモ
https://chaika.hatenablog.com/entry/2019/01/22/133858

実際にアプリをリリースする前に不安な方は[Firebase リリース チェックリスト](https://firebase.google.com/support/guides/launch-checklist?hl=ja) を確認しましょう。


## Authenticationの実装

**一番悩みました** はじめはVuexfireを使おうと考えましたが、ログインしたユーザーをどう結びつければよいのか、認証状態をどうやって管理するかサンプルをかなり調べてうんうん唸っていました。
さらには、AuthenticationでTwitterアカウントの認証を完了したとき、書き方によってはログインしてないとみなされる問題でハマりました。
そもそもVuexfireではAuthenticationのことは何も触れていません。

結論としては、こちらの例のように **Vuexfireを使わずに** Vuexを用いて

 * stateでAuthenticationの認証状態を管理して
 * actionでFirestoreの読み書き、更にはAuthenticationの認証状態を確認するなど…

をまとめたほうが、アプリケーション全体で認証情報にアクセスできて良さそうだと考えました(個人の考えです)

Nuxt.js と Firebase（Firestore）を使って認証と DB 保存を実装する - Qiita
https://qiita.com/ryamakuchi/items/ec71a20c45b32a382ef9

また、ログインする際にFirebase Authentication → Twitterのログイン画面 → 元のアプリの画面とリダイレクトさせる方法をお勧めしていますが、リダイレクトした時の認証情報の扱いもこちらのほうがすんなり対応できた気がします(要検証)

> Twitter プロバイダ オブジェクトを使用して Firebase での認証を行います。ユーザーに Twitter アカウントでログインするよう促すために、ポップアップ ウィンドウを表示するか、ログインページにリダイレクトします。モバイル端末ではリダイレクトすることをおすすめします。
> 
> https://firebase.google.com/docs/auth/web/twitter-login?hl=ja

### VuexストアにAuthenticationを実装する

Vuexストアである `./store/index.js` を作ります。
Authenticationを操作している部分は次のとおりです。

Changedとは言っていますが、認証状態を確認するために `onAuthStateChanged` 関数を呼ぶ必要があります。ログインするアクションを起こしていなくともログイン状態を確認するのであればこの関数が入ったactionを呼ぶことになります。 ~~気持ち悪いと感じている人がいてよかった~~

```js:./store/index.js
...
export const actions = {
  twitterSignIn({ dispatch }) {
    firebase.auth().signInWithRedirect(new firebase.auth.TwitterAuthProvider())
    dispatch('twitterAuthStateChanged')
  },
  twitterSignOut({ dispatch }) {
    firebase.auth().signOut()
    dispatch('twitterAuthStateChanged')
  },
  twitterAuthStateChanged({ dispatch, commit }) {
    firebase.auth().onAuthStateChanged((user) => {
      if (user) {
        const { displayName, uid } = user
        commit('storeAuthInfo', {
          userName: displayName,
          firebaseUid: uid
        })
      } else {
        commit('deleteAuthInfo')
      }
    })
  },
...
```

 * stateで「ログインしていない」初期値を定義して
 * actionsでFirebase Authenticationの `firebase.auth().signInWithRedirect(new firebase.auth.TwitterAuthProvider())` を呼び出しログインさせ
 * `firebase.auth().onAuthStateChanged` の結果を見てstateを変更するMutationsを呼ぶ(commit)

Vuexストアにログイン情報を登録することで、こちらの例のようにNuxtのコンポーネントから `$store.state` でログイン状態にアクセスできます。

```vue:Vuexストアに保存したログイン情報を使う例
<template>
    ...
    
        <!--- ログインしていない状態 --->
        <button
          v-if="!$store.state.isLoggedin"
          @click="signinWithTwitter"
          class="button is-info is-large"
        >
          Twitterアカウントでログイン
        </button>
    
        <!--- ログインしている状態 --->
        <div v-if="$store.state.isLoggedin">
          ようこそ、 {{ $store.state.userName }}さん。
          <button @click="signOut" class="button is-info is-large">
            ログアウト
          </button>
        </div>
```

こちらのコンポーネントからVuexストアのActionsを呼び出すには、 `@click` イベント -> `methods` -> `this.$store.dispatch()` で呼び出せます。
また、ページが読み込まれたとき(Vueがマウントされたとき)にもAuthenticationのログイン状態を確認します。

```vue:VuexのActionsを呼び出す例
<script>
export default {
  mounted() {
    this.$store.dispatch('twitterAuthStateChanged')
  },
  methods: {
    signinWithTwitter() {
      this.$store.dispatch('twitterSignIn')
    },
    signOut() {
      this.$store.dispatch('twitterSignOut')
    }
  }
}
</script>
```

実装できたら `yarn dev` で試してみてください！ ただし、**ログインが完了してアプリに戻っても数秒間表示が変化しないことがあります。**

### Uncaught FirebaseError: projectId must be a string in FirebaseApp.options と表示される場合

`yarn dev`したローカルのURLをブラウザーで開いても何も表示されず、コンソールを見てみると次のエラーで止まってしまうことがあります。

> Uncaught FirebaseError: projectId must be a string in FirebaseApp.options

まずは`.env`に記述してあるFirebaseの接続情報が正しく読み込めているかどうかを確認してください。また、`.env` はデフォルトで`.gitignore`に登録されているため、GitHubにpushして他のマシンでclone……なんてことをすると **認証情報がすっかり抜けてcloneされ、`.env`を新規作成する必要があります。**

## コンソールでFirestoreをスタートさせる

コンソールからDatabaseを選び開始ボタンをクリックすると、はじめのセキュリティルールの設定が出ます。
![firebasse-7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/dcb7e0b8-c3fc-06c2-87b6-ac5bcf30ed93.png)

セキュリティルールは **テストモードで開始** を選択します。このルールを変更すると「Firebase Authenticationでログインしたユーザーだけ使える」ということができますが、初期の開発でFirestoreが使えると分かるまではテストモードにして「ルールが原因で動かない」ということを避けましょう。**大丈夫です、後で直します**

(Hostingを開始させてしまうと公開されるFirebaseの認証情報を悪用して、テストモードでガバガバなFirestoreに向けてガンガン攻撃されるかもしれないので、ルールをしっかりさせるまではHostingをやめておきましょう)

次に進んで質問されたFirestoreのリージョンは `nam5(us-central)`を選びました。

Firestoreの作成が終わると、コレクションを追加する画面になります。Nuxt.jsからデータが取得できるかどうか確かめるために仮のデータをここで入れます。「+コレクションを開始」をクリックして `board1` コレクションを作成します。
![firebase-8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/15cc4111-5b17-2183-60c9-edc16c6611a7.png)


コレクションを作成するとドキュメントを追加する画面になるので、次のようにフィールドを設定します。値は自分で考えてください(適当)
**ここではドキュメント IDは「自動ID」をクリックします。後でドキュメントIDにはAuthentication独自のユーザーIDを投入します。**
![firebase-9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/7692cc2f-64f0-4a78-e31c-eda1c919fa4f.png)

## Cloud Firestoreの実装

再びVuexストアに戻ります。Vuexストアの使い方はAuthenticationと似ていますが、

 * Nuxtのコンポーネント側からVuexのactionをdispatchして
 * VuexのactionでFirebaseを操作して
 * 操作した結果をVuexのmutationにcommitで渡して
 * VuexのmutationがVuexのstoreを変更して
 * Nuxtのコンポーネントがそれを読み取り投稿した結果がユーザーに届く

実装になります。ここではFirebaseの操作部分について説明します。

### すべてのユーザーの投稿を読み取る

こちらはログインしなくても見られるようにしたいので、Nuxtコンポーネントが読み込まれた(マウントされた)段階から実行します。

コンソールからFirestoreにデータを追加した際に `コレクション ー ドキュメント ー フィールド` の構造になっていたのに気づいたかもしれません。コレクションに対して `get()` をすると `querySnapshot` の形でコレクション内の全ドキュメントが取得されます。

```js:./store/index.js
...
    const recvMessages = []
    firestoreDb
      .collection('board1')
      .get()
      .then((querySnapshot) => {
        // Firestoreからやってきたデータを扱いやすい形に変換する
        querySnapshot.forEach(function(doc) {
          recvMessages.push({ id: doc.id, data: doc.data() })

          // 前に投稿したものがある場合は投稿フォームを隠す
          if (doc.id === state.firebaseUid) {
            commit('setPosted', true)
          }
        })
      })
      .catch((error) => {
        console.error('Error getting document:', error)
      })
      .finally(function() {
        // 成功した場合はメッセージのリストを、失敗したときは空のリストを使って表示させる
        console.log(recvMessages)
        commit('updateDisplayMessage', recvMessages)
      })
...
```

### 投稿を追加・更新する

ドキュメントIDにはAuthenticationでユーザーごとに割り振られた `uid` を用います。ドキュメントIDを指定するには `doc()` を使います。


```js:./store/index.js
...
  firestoreMessageAdd({ state, commit }, payload) {
    const postedDate = new Date()
    const postedTimestamp = Math.floor(postedDate.getTime() / 1000)
    firestoreDb
      .collection('board1')
      .doc(state.firebaseUid)
      .set({
        userName: state.userName,
        comment: payload.messageText,
        date: postedTimestamp
      })
      .then(() => {
        console.log('Document successfully written!')
        commit('setPosted', true)
      })
      .catch((error) => {
        console.error('Error writing document: ', error)
      })
      .finally(() => {
        // 成功しようが失敗しようが最新の状態を取得する
        this.dispatch('firestoreMessageCheck')
      })
  },
...
```

「新規登録は1度きり」を実現するために、今回は1度投稿すると新規投稿フォームはフロントエンド側で隠します(データベース側の制限は後ほどの「セキュリティルール」で設定します)
仮に同じIDを持つドキュメントに対してFirestoreの `set()` を実行すると **ドキュメント全体の上書きになります。** ドキュメントのフィールドだけを更新する場合は `update()` を使います。

また、投稿が終わった際に最新の情報を取得するために再読込しています。大規模になってくると繰り返しコレクション全体を読み込むのは辛いので、最後に読み込んでから新しいものだけを追加する処理が必要になってきます。


### 投稿を削除する

コレクションとドキュメントIDを指定して `delete()` を呼び出すだけです。 ~~前は一旦検索してから削除したような気が~~
こちらも例によってアクション終了時に最新のメッセージを取得しています。

```js:./store/index.js
...
  firestoreMessageDelete({ state, commit }) {
    firestoreDb
      .collection('board1')
      .doc(state.firebaseUid)
      .delete()
      .then(function() {
        console.log('Document successfully deleted!')
        commit('setPosted', false)
      })
      .catch(function(error) {
        console.error('Error removing document: ', error)
      })
      .finally(() => {
        // 成功しようが失敗しようが最新の状態を取得する
        this.dispatch('firestoreMessageCheck')
      })
  }
  ...
```

## Firestoreのセキュリティルールを設定する

これで完成したように見えますがまだまだ足りません。公開するにはFirestoreの操作を制限しなければなりません。
Firebaseのコンソールを開いて[Database]->[ルール]を選択するとセキュリティルールを編集できます。
![firebase-10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/e8488a2c-365e-3ee5-ff7d-db336f050acf.png)


### ログインしていないと書き込めないようにする

```
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read;
    }
    match /board1/{postID} {
      allow create: if request.auth.uid != null;
    }
  }
}
```
何も設定しないと「拒否」になるため、セキュリティルールには許可する操作を記述します。
また、1ドキュメントに複数のセキュリティルールを設定すると OR で評価されます。

 * はじめのワイルドカードで全員がドキュメントを見られるようにして
 * ドキュメントを作成する `create` にはAuthenticationで認証されたIDがあるかどうかをチェックしています

 `match` で指定する場所には `{}` で囲むと任意で変数を設定できます。ここではドキュメントのIDを変数にしています。


### 自分が投稿したドキュメント以外変更できないようにする

```
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read;
    }
    match /board1/{postID} {
      allow write: if request.auth.uid == postID;
    }
  }
}
```

先程変数にしたドキュメントのIDとAuthenticationで認証したFirebase内部のIDをマッチさせて自分の投稿かどうかをチェックしています。
そしてルールに `read` があるのなら `write` もあるのでは？ルールで `write` を指定すると `create`, `update`, `delete` の操作が対象になります。

実はこの設定にすると「ログインしないと書き込めないようにする」も達成できます。ログインしていない状態では `request.auth.uid` が `null` になって一致しなくなるためです。今回はこの設定でおしまいです。

余裕があればセキュリティールールに反した書き込みを試してみて、Webブラウザの「コンソール」から書き込みが拒否されたときのメッセージを確認してみてください🙄


### ルールシミュレータについて

左側にあるルールシミュレータでセキュリティルールを試すことができます。「許可されました」「拒否されました」と操作の結果が表示されるだけではなく、認証状態をセットしてシミュレーションもできるため便利です。

シミュレータに入力する「場所」には

 * コレクションが `board`
 * ドキュメントIDが `documentid1`

の場合 **`/board/documentid1` と指定します。** `/databases/(default)/documents`が入力のサンプルだと勘違いして10分ほどハマりました。

[Firestoreのドキュメントにあるサンプル](https://firebase.google.com/docs/firestore/security/rules-conditions?hl=ja#authentication)の形でルールを設定してシミュレータで「認証なし」にすると「拒否されました」ではなく「Null value error」と表示されるのは **謎です** (実際に書き込みを試してみるとMissing or insufficient permissions.になるため、正しく操作を制限できています)

![firebase-11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/2c831533-2879-2090-27c5-621309c548f1.png)


## おわりに

Twitterアカウントでログインして、一言残せるようなサービスを作ることができました。
Authenticationでログインしたときに認証状態が反映されるのに時間がかかるためその間の表示をどうするか定める必要がある、投稿が増えてきたときにFirestoreとのやり取りを減らす必要があるなど改善点はたくさんあります。

初めて触るとハマりどころが多いのですが、ユーザーの認証からデータのやり取りまで用意されていてしかも認証状態とデータを簡単につなげることができることができるのが魅力です。今回のアプリであればHostingでホストして公開まで持っていけます。

~~あれ、全投稿をVuexストアに詰めたら良くない気が…~~

## 参考

Nuxt.jsで手間取ったことまとめ - Qiita
https://qiita.com/ztrehagem/items/3c4accf04aa458b9f22e

Nuxt.js と Firebase（Firestore）を使って認証と DB 保存を実装する - Qiita
【改訂版】 Firebase Cloud Firestore rules tips
https://tech-blog.sgr-ksmt.org/2018/12/11/194022/

Firebase Cloud Firestoreのデータ更新 setとupdateの違い - ブロックチェーンエンジニアとして生きる
https://tomokazu-kozuma.com/difference-between-set-and-update-when-updating-cloud-firestore-data/