Vue.js (Vuex, vue-router, vuetify) とFirebaseで始めるユーザー管理

# 概要

昨今、バックエンドがAPIサーバーであることも増えてきたが、認証機能の実装は面倒なことが多い。そのため、認証機能をFirebase Authenticationに任せることも考えられる。本記事では、人気のJavaScriptフレームワークであるVue.jsでフロントエンドのログイン機能を実装し、Firebase Authenticationと連携させる。
また、Vuexを用いたユーザー情報などの状態管理、vue-routerによるルーティングを行い、UIコンポーネントフレームワークとしてVuetifyを使用した。

本記事で説明するシステムは以下の通りです。"/"でホーム画面が描画され、ログイン状態のとき"/test"でもホーム画面が描画される。一方で、ログインしていないときの"/test"では、"/signin"にリダイレクトされログイン画面が表示される。また、"/signup"でサインアップが描画される。
ログイン、サインアップは、firebaseとの連携により実行される。また、ログイン状態は、Vuexにより管理される。

本記事のサンプルは、以下のバージョンを用いた。

```bash
npm install -g @vue/cli
vue --version
> 3.10.0
```

git: [k-washi/example-vue-cli](https://github.com/k-washi/example-vue-cli)の一部が本記事に対応しています。

# Vue.jsの設定

本章では、Vue.jsのプロジェクトを作成し、Vuex, vue-router, vuetifyを用いて作成されたプロジェクトを起動させます。

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

以下のコマンドでプロジェクトを作成する。このとき、Vuex, vue-routerも同時にダウンロードする。

```bash
#projectの作成
vue create sample-vue

# 以下、選択肢 (例なので適宜変更してください)
#>Mannually

#>Vuex, Routerを選択 
#(他にも必要なパッケージがあれば選択する。選択例 Babel, Router, Vuex, Linter)

#U>se history mode for router / Y (default)

#>ESLint * Prettier
#>Lint on save
#>In package.json
```

## 2. Vuetifyのダウンロード

```bash
#package.jsonがあるディレクトリにて
vue add vuetify
#>Default

#もし iconを使用するなら
npm install @mdi/font -D
npm install @mdi/js -D
```

## 3. プロジェクトの実行とビルド

以下のコマンドでプロジェクトを実行し、ブラウザでApp running atに記載されたIPにアクセスする。

```bash
npm run serve
```

また、以下のコマンドでビルドすることで、distファイルの配下にindex.htmlやcssファイルなどが作成される。

```bash
npm run build
```
以下の画面が描画結果です。

![スクリーンショット 2019-11-05 0.17.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481395/cc63c5e7-fed1-439f-f75b-b824d5b9e74f.png)
# Firebaseの設定


## Firebaseのプロジェクト設定

  1. [Firebase](https://firebase.google.com/?hl=ja)にてアカウントを作成し、コンソールに移動する。
  2. プロジェクトを追加する。
  3. Project Overviewの</>マークからアプリを登録する (Firebase Hostingはチェックしない)
  4. Authenticationのログイン方法を設定(今回は、メール/パスワードを有効にする)
  

## Vue.jsにインストール

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

# ユーザー管理処理の実装

まず、前提として、emailとパスワードを用いてfirebase側で認証される。そして、その結果、改善検知が可能なJWTが得られる。
ここでは、firebaseと連携したユーザー管理機能として、ユーザーの作成、ログイン、ログアウト、状態確認の機能を実装した。

## Vuexを用いたユーザーのログイン状態管理

stateに管理する状態を列挙する。そして、この状態を取得するgettersと状態を更新するmutationsを実装する。
ここでは、emailとログイン状態を管理することとする。

```javascript:store.js
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  strict: true,
  state: {
    email: "",
    status: false,
  },
  getters: {
    email(state) {
      return state.email
    },
    isSignedIn(state) {
      return state.status
    },
  },
  mutations: {
    onAuthEmailChanged(state, email) {
      state.email = email; //firebase user情報
    },
    onUserStatusChanged(state, status) {
      state.status = status;
    }
  },
});
```

## Firebaseとの連携部分

まずは、Firebaseと連携するための設定を行う。Project Overviewプロジェクト設定のFirebase SDK snippetに記載されているfirebaseConfigをsample-vue/.env.localに記載する。この.envファイルは、VUE_APPから開始する変数を設定できる。

```configuration:.env.local
    VUE_APP_FB_KEY=AIzaxxxxxxxxxx
    VUE_APP_FB_DOMEIN=ex-firebase-auth
    VUE_APP_FB_ID=https://ex-firebase-auth
    VUE_APP_FB_PROD_ID=https://ex-firebase-auth
    VUE_APP_FB_BUCKET_ID=ex-firebase-auth.appspot.com
    VUE_APP_FB_SENDER_ID=661xxx18x2
```

次に、firebaseと連携する処理を実装する。
詳細はコード内に記載。

```javascript:firebase.js

import firebase from "@firebase/app";
import "@firebase/auth";
import router from "./router"
import store from "./store";
import { createSecretKey } from "crypto";

//process.envでenvファイルに定義した変数にアクセスできる
const firebaseConfig = {
  apiKey: process.env.VUE_APP_FB_KEY,
  authDomain: process.env.VEW_APP_FB_DOMEIN + ".firebaseapp.com",
  databaseURL: process.env.VUE_APP_FB_ID + ".firebaseio.com",
  projectId: process.env.VUE_APP_FB_PROD_ID,
  storageBucket: process.env.VUE_APP_FB_BUCKET_ID,
  messagingSenderId: process.env.VUE_APP_FB_SENDER_ID,
};

export default {
  //読み込み時に、firebase機能の設定をする
  init() {
    firebase.initializeApp(firebaseConfig);
    firebase.auth().setPersistence(firebase.auth.Auth.Persistence.SESSION);
  },

  //emailとパスワードでログインする。
  //firebase認証の結果、JWT Tokenが返ってくる。JWTは、localstrageに保存する。
  //ここで、記載しているrouterは、vue-routerの機能で、'/'へルーティングしている
  signInWithEmailAndPassword(email, password) {
    firebase.auth().signInWithEmailAndPassword(email, password)
      .then(res => {
        res.user.getIdToken().then(idToken => {
          localStorage.setItem('jwt', idToken);
          router.push('/').catch(err => {
            console.log("router push /");
          });
        })
      }, err => {
        console.log(err.message);
      })
  },
  //emailとパスワードでアカウント作成する
  //アカウント作成後は、'/signin'へルーティング
  signUpWithEmailAndPassword(email, password) {
    firebase.auth().createUserWithEmailAndPassword(email, password);
      .then(res => {
        router.push('/signin');
      }).catch(err => {
        console.log(err.message);
      })
  },
  //ログアウト
  //ログアウト後は、保存しているjwtを削除して、vuexのmutationに実装した状態の更新処理でユーザーをログアウト状態にする。
  logOut() {
    firebase.auth().signOut()
      .then(() => {
        localStorage.removeItem("jwt")
        store.commit('onAuthEmailChanged', "");
        store.commit('onUserStatusChanged', false);
      })
      .catch((err) => {
        console.log(`fail logout (${error}) `);
      })
  },
  //状態管理
  //jwtの状態、ユーザーの状態を更新する。
  onAuth() {
    firebase.auth().onAuthStateChanged(user => {
      if (user) {
        if (user.ma) {
          localStorage.setItem('jwt', user.ma);
        } 
        store.commit('onAuthEmailChanged', user.email);
        if (user.uid) {
          store.commit('onUserStatusChanged', true); 
        } else {
          store.commit('onUserStatusChanged', false);
        }
      } else {
        store.commit('onAuthEmailChanged', "");
        store.commit('onUserStatusChanged', false);
      }
    })
  }
}
```

次にfirebaseとの連携処理にも出てきたルーティング機能を実装する。
今回は、簡単のため、ログイン状態でアクセスしなければならないパスを/testとし、ログイン後は/testでホーム画面が描画されるものとする。
routersにpathとともに、描画するコンポーネントを記載することで, コンポーネント内に記載された```<router-view></router-view>```の場所にルーティングのコンポーネントを描画する。

path:"*"は、一致するpathがなかった場合のリダイレクトのために設定している。
scrollBehaviorは、戻る処理をしたときに、表示する位置を遷移以前の位置で再度描画するための処理を行う。
router.beforeEacは、ルーティング時のミドルウェアの役割であり、ルーティング処理が実行されるたびに呼び出される。Firebase.onAuth()でユーザーの状態を確認し、選択されたパスへルーティングする。また、meta: {requiresAuth: true}が付随したパスであり、かつ、未ログイン状態の場合、ログイン画面へルーティングされる。

```javascript:router.js
import Vue from "vue";
import Router from "vue-router";

import Home from "@/views/Home.vue";
import Signup from "@/components/FBSignup.vue";
import Signin from "@/components/FBSignin.vue"

import Firebase from "./firebase"
import store from "./store"

Vue.use(Router);

const router = new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    {
      path: "/",
      name: "home",
      component: Home
    },
    {
      path: "/test",
      name: "test",
      component: Home,
      meta: {requiresAuth: true},
    },
    {
      path: "/signup",
      name: "signUp",
      component: Signup,
    },
    {
      path: "/signin",
      name: "signIn",
      component: Signin,
    },
    {
      path: "*",
      redirect: '/'
    },
  ],
  scrollBehavior(to, from, savedPosition) {
    //戻るによる遷移は以前の位置を保持
    if(savedPosition) {
      return savedPosition
    } else {
      if (to.hash) {
        return {selector: to.hast}
      } else {
        return {x: 0, y: 0}
      }
    }
  }
});

router.beforeEach((to, from, next) => {
  Firebase.onAuth();
  let currentUserStatus = store.getters["isSignedIn"];
  let requiresAuth = to.matched.some(record => record.meta.requiresAuth);
  if (!requiresAuth ) {
    next()
  } else if (requiresAuth && !currentUserStatus) {
    next('/signin');
  } else {
    next();
  }
  
})
export default router
```

最初に読み込んだときに実行する処理をmain.jsに実装した。
ここでは、firebaseの初期化をするとともに、vuex, vue-router, vuetifyをアプリに登録している。
また、firebase.auth().onAuthStateChanged()は、ブラウザの更新を行ったときなどに、非同期に実行されるユーザー情報の更新を描画前に実行するために実装している。

```main.js
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import vuetify from "./plugins/vuetify";
import firebase from "firebase/app"

Vue.config.productionTip = false;

Firebase.init();

firebase.auth().onAuthStateChanged(user => {
  if (user) {
    if (user.ma) {
      localStorage.setItem('jwt', user.ma);
    } 
    store.commit('onAuthEmailChanged', user.email);
    if (user.uid) {
      store.commit('onUserStatusChanged', true) 
    } else {
      store.commit('onUserStatusChanged', false)
    }
  } else {
    store.commit('onAuthEmailChanged', "")
  }
  new Vue({
    router,
    store,
    vuetify,
    render: h => h(App)
  }).$mount("#app");

})

```

最後に、ログイン画面のコンポーネントについて説明する。
本コンポーネントは、ログイン状態の場合、ユーザー情報が描画され、ログイン状態ではない場合、ログインフォームが描画される。userStatusでユーザーのログイン状態を取得しており、usernameでユーザーのemail情報を取得している。

v-*はvuetifyによるコンポーネントです。
```<v-btn class="ma-2" outlined color="deep-orange" to="/">```はボタンコンポーネントでto="/"でボタンを押した際に移動するパスを指定している。
また、```<v-btn class="ma-2" outlined color="deep-orange" @click="signIn">Sign In</v-btn>```はクリック時に、methodsに定義したsignInメソッドを実行する。signInは単純にFirebase.jsに定義したログイン関数を使用している。(本当は、フォーム内容で条件分岐が必要かもしれません...)


```vuejs:FBSignin.vue
<template>
  <div>
    <v-btn class="ma-2" outlined color="deep-orange" to="/">
      Home
    </v-btn>
    <p>アカウントを持っていない場合</p>
    <v-btn class="ma-2" outlined color="deep-orange" to="/signup">
      Sign Up
    </v-btn>
  <div v-if="userStatus" key="login">
    <h2>{{username}} がログイン済みです</h2>
  </div>
  <div v-else key="login_process">
    <h2>Sign In</h2>
    <v-form ref="form" v-model="valid" :lazy-validation="lazy">
      <v-col cols="12" sm="6">
        <v-text-field 
          v-model="email" 
          label="email" 
          type="email" 
          required
          ></v-text-field>
      </v-col>
      <v-col cols="12" sm="6">
        <v-text-field 
          v-model="password" 
          label="password" 
          type="password" 
          hint="At least 8 characters"
          :rules="[rules.required, rules.min]" 
          required
        ></v-text-field>
      </v-col>
    <v-btn class="ma-2" outlined color="deep-orange" @click="signIn">Sign In</v-btn>
    
    </v-form>
  </div>
  </div>
</template>

<script>
import Firebase from "../firebase"
export default {
  name: 'Signin',
  data: () => ({
      email: '',
      password: '',
      rules: {
          required: v => !!v||'Required.',
          min: v => v.length >= 8||'Min 8 characters',
      },
      valid: true,
      lazy: false,
  }),
  computed: {
    username() {
      return this.$store.getters.email
    },
    userStatus() {
      //return true in login state
      return this.$store.getters.isSignedIn
    },
  },
  methods: {
    signIn() {
      Firebase.signInWithEmailAndPassword(this.email, this.password);
    },
  },
}
</script>
```

# Appendix

アカウント作成画面などは、ログイン画面と酷似しているため説明を省いた。今回の内容と一致しないが、追加で、[k-washi/example-vue-cli](https://github.com/k-washi/example-vue-cli)に、様々な処理を実装しているので参考にしてみてください。