【４】Firebase AuthenticationとNuxt.jsのVuexで認証機能を実現する(2)

## はじめに

前回で、Nuxt.js上でVuexを使って状態を取得・更新できるようになった。

さきにページ遷移の準備を済ませてから、実際にFirebaseを使った認証を行う。

忘れないうちにfirebaseのインストールを済ませておく。

```bash
npm install firebase --save
# またはyarn add firebase
```

## 手順

### ログインページを作成する

Nuxt.jsでは `pages` ディレクトリにvueファイルを入れれば、ルーティングはやってくれる。
なので `pages/login/index.vue` を新規作成して、シンプルなページを作る。

```pages/login/index.vue
<template>
  <v-layout column justify-center align-center>
    <v-flex xs12 sm8 md6>
      <div class="text-center">
        <logo />
        <vuetify-logo />
      </div>
      <v-card>
        <v-card-title class="headline">Login</v-card-title>
        <v-card-text>
          <v-btn>signIn</v-btn>
        </v-card-text>
      </v-card>
    </v-flex>
  </v-layout>
</template>


<script>
import Logo from "~/components/Logo.vue";
import VuetifyLogo from "~/components/VuetifyLogo.vue";

export default {
  components: {
    Logo,
    VuetifyLogo
  }
};
</script>
```

http://localhost/login へ飛ぶと、実際にログインページが表示される。

### middlewareをつかってルーティングを設定する

「ログインされていなかったら、自動でログインページにリダイレクトする」ことを実現する。
こういったルールを作れるのがNuxt.jsのmiddlewareという仕組み。

まず認証に関するルーティングを担うロジックをつくる。

```middleware/authenticated.js
export default function ({
  store,
  route,
  redirect
}) {
  if (route.name !== 'login') {
    redirect('/login')
  }
}
```

次にnuxt.config.jsに、middlewareを利用する旨を追記する。

```diff
export default {
  // 省略
+  router: {
+    middleware: 'authenticated'
+  },
}
```

こうすることで、http://localhost/login 以外（トップページ）に飛ぶとログインページにリダイレクトさなる。

### .env(dotenv)を設定する

FirebaseのSDKスニペットを参考に、下記の設定を準備する

```.env
BASE_URL=http://localhost:3000
FB_API_KEY="*****" // apiKeyを入れればいい。以下同様。
FB_AUTH_DOMAIN="*****"
FB_DATABASE_URL="*****"
FB_PROJECTID="*****"
FB_STORAGE_BUCKET="*****"
FB_MESSAGING_SENDER_ID="*****"
FB_APP_ID="*****"
```

### pluginsを追加する

認証関連の設定、ロジックをプラグインとして作成する。

> Nuxt.js では JavaScript プラグインを定義することができ、それはルートの Vue.js アプリケーションがインスタンス化される前に実行されます。この機能は、自前のライブラリや外部のモジュールを使用する際にとりわけ有用です。
> https://ja.nuxtjs.org/guide/plugins/

まずはfirebaseの設定をする。
.envで設定した情報で初期化したプラグインを作成。

```plugins/firebase.js
import firebase from "firebase/app";
import "firebase/auth";

const firebaseConfig = {
  apiKey: process.env.FB_API_KEY, // こう書くことで.envファイルを見てくれる
  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_API_ID
};

if (!firebase.apps.length) {
  firebase.initializeApp(firebaseConfig);
}

export default firebase;
```

### Google認証を実装する

`store/user.js` の `signIn` アクションを実装する。

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

const auth = firebase.auth()

export const state = () => ({
  isSignedIn: false // ログインしているか
});

// ゲッター。computedのような感じで、変数が変わったときだけ再評価される。
export const getters = {
  isAuthenticated: state => !!state.isSignedIn
};

// ミューテーション。ストアの状態を変更する。コミットが必要。
export const mutations = {
  setSignInState(state, isSignedIn) {
    state.isSignedIn = isSignedIn;
  }
};

// アクション。状態を変更するのではなく、ミューテーションをコミットする。
// 任意の非同期処理を含むことができる。
export const actions = {
  async signIn({
    commit
  }) {
    try {
      // Google認証プロバイダを使ってポップアップ形式でサインインする。
      // Providerを変えれば他のサービスでも認証可能らしい。
      const provider = new firebase.auth.GoogleAuthProvider();
      await auth.signInWithPopup(provider).then(ret => {
        commit("setSignInState", true);
      }).catch(error => {
        console.error(error);
      })
    } catch (error) {
      console.error(error)
    }
  },
  signOut({
    commit
  }) {
    commit("setSignInState", false);
  }
};
```

上記のsignInアクションを使うようにログインページを改修する。
認証状態がわかるように、getterMapも利用。

```diff
<template>
  <v-layout column justify-center align-center>
    <v-flex xs12 sm8 md6>
      <div class="text-center">
        <logo />
        <vuetify-logo />
      </div>
      <v-card>
        <v-card-title class="headline">Login</v-card-title>
        <v-card-text>
+          <v-btn @click="signIn">signIn</v-btn>
+          <p>isAuthenticated: {{ isAuthenticated }}</p>
        </v-card-text>
      </v-card>
    </v-flex>
  </v-layout>
</template>

<script>
+import { mapGetters, mapActions } from "vuex";

import Logo from "~/components/Logo.vue";
import VuetifyLogo from "~/components/VuetifyLogo.vue";

export default {
  components: {
    Logo,
    VuetifyLogo
  },
+  computed: {
+    ...mapGetters("user", ["isAuthenticated"])
+  },
+  methods: {
+    ...mapActions("user", ["signIn"])
+  }
};
</script>
```

この状態でログインページに飛んでみると、「isAuthenticated: false」となっているはず。

そして「SIGNIN」ボタンをクリックすると、いつものGoogleのページがポップアップで開く。
この認証に成功すると、「isAuthenticated: true」となる。

### 認証状況を判断する

**いろいろとサイトを参考にさせてもらいましたがベストプラクティスがわからなかったため、まずは動くものを作っています。**

このままだと再度ログインページに飛んだときに、また未認証に戻ってしまう（おそらくStoreの状態たちが初期化されるあｋら）。
そのため認証状況の判断を入れてルーティングをやりなおす。

まずは認証状況を判断するプラグインを作成する。

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

const auth = () => {
  return new Promise((resolve, reject) => {
    console.log("plugins/auth");
    firebase.auth().onAuthStateChanged(user => {
      resolve(user || false);
    })
  })
}
export default auth
```

これをauthenticated.jsで利用する。

```app/middleware/authenticated.js
import auth from '~/plugins/auth'

export default async function ({
  store,
  route,
  redirect
}) {
  // Storeが持っている認証状態を確認
  let isAuthenticated = store.getters['user/isAuthenticated'];

  // 未認証の状態の場合、Firebaseの認証状況を確認して最新の状況を取得。
  if (!isAuthenticated) {
    await auth().then(user => {
      if (user) {
        // Firebase上では承認ができているので、Storeも認証済に更新する
        isAuthenticated = true;
        store.dispatch('user/setSignedIn', true);
      }
    });
  }

  // 未認証かつログインページ以外に飛ぼうとしている場合
  if (!isAuthenticated && route.name !== 'login') {
    redirect('/login');
  }

  // 認証済だがログインページに飛ぼうとしている場合は、トップページにリダイレクト
  if (isAuthenticated && route.name === 'login') {
    redirect('/')
  }

  // そのほかは、 指定されたページにそのままいくようになる
}
```

上で `setSignedIn` を定義している通り、シンプルにStoreの認証状態を更新するアクションも store/user.js に追加する。

```diff
export const actions = {
  async signIn({
  // 省略
+  async setSignedIn({
+    commit
+  }, signedId) {
+    console.log("user/setSignedIn");
+    await commit("setSignInState", signedId);
+  },
};
```

これで http://localhost/login に飛ぶと、さきほどログインしたことが考慮されてトップページにリダイレクトされる。

### ログアウトする

最後にログアウトも実装する。
まずは store/user.js から。

```diff
export const actions = {
  async signIn({
  // 省略
  async setSignedIn({
  // 省略
+  async signOut({
+    commit
+  }) {
+    auth.signOut().then(() => {
+      commit("setSignInState", false);
+    })
+  }
};
```

http://localhost 上で「SIGNOUT」を押すと、ログアウトされる。ページを更新すれば、ログインページにリダイレクトされるはず。

### ログイン／ログアウト後のページ遷移

このままだとログイン／ログアウトしたあとにページが遷移しないので、これをなんとかする。
`this.$router.push` でリダイレクトができるので、 store/user.js で利用します。

```diff
export const actions = {
  async signIn({
    commit
  }) {
    try {
      const provider = new firebase.auth.GoogleAuthProvider();
      await auth.signInWithPopup(provider).then(ret => {
        commit("setSignInState", true);

+        // ログインに成功したらトップページへリダイレクト
+        this.$router.push("/");
      }).catch(error => {
        console.error(error);
      })
    } catch (error) {
      console.error(error)
    }
  },
  // 省略
  async signOut({
    commit
  }) {
    await auth.signOut().then(() => {
      commit("setSignInState", false);

+      // ログアウトに成功したらログインページへリダイレクト
+      this.$router.push("login");
    })
  }
};
```

ここまでを終えると、google認証機能を持ったPWAアプリが完成する。
おつかれさまでした。

## ここまで

https://github.com/koboriakira/sparrow-memo/tree/ver1.4