入門: Vue.jsで認証機能をモジュール化する

# はじめに

「[入門: Vue.jsで認証機能を作る](https://qiita.com/koduki/items/b4b56a27c6b7406a4ddb)」でトークン認証を、「[Vue.jsとIdentity Platform(IDプラットフォーム)でAPIの認証機能を作る](https://qiita.com/koduki/items/54ce1c6cb636357a6e26)」を作成してきました。

この2つの記事の内容を組合わせればIdentity Platformを使った認証が出来るわけですが、前回は基本的にはモジュール化せずにべた書きしていたので、今回は別ファイルに分けてモジュール化してみました。これで大分管理がすっきりしまた。

という分けで今回も備忘録を兼ねた解説記事となります。

# Vuexのモジュール分割

以前の記事ではVuexのモジュールを使わずに書いていたのでトップレベルの名前空間を使っていたのでprefixを付けていました。

```js
const store = new Vuex.Store({
    state: {
        userId: "",
        userToken: ""
    },
    mutations: {...},
    actions: {...},
    modules: {
    },
})
```

Vuexのモジュール機能を使って以下のように書きます。

```src/sotre/index.js
import Vue from "vue";
import Vuex from "vuex";

import user from "@/store/modules/user";

Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    user,
  },
});

export default store;
```

モジュールに`user`を読み込んで追加しています。`user.js`は以下の通り。

```src/store/modules/user.js
export default {
  namespaced: true,
  state: {
    id: "",
    name: "",
    token: "",
    pic: "",
    expiration_time: "",
  },
  mutations: {
    store(state, user) {
      state.id = user.id;
      state.token = user.token;
      state.name = user.name;
      state.pic = user.pic;
      state.expiration_time = user.expiration_time;
    },
    drop(state) {
      state.id = state.token = state.name = state.pic = state.expiration_time = "";
    },
  },
  actions: {
    store(context, user) {
      context.commit("store", user);
    },
    drop(context) {
      context.commit("drop");
    },
  },
};
```

これで`{{$store.state.user.id}}`のように取り扱えるので名前空間を活用出来て良いですね。単独のstateで管理すると破綻は見えてるので、この方法で分割するのが基本なのだと思います。

# ビジネスロジックをモジュール化する

前回はIdentity PlatformとやりとりするFirabase SDKの処理をそのままVueファイルに記載していました。ただ、これはプレゼンテーションロジックとビジネスロジックが混在するという意味で基本的によろしくないです。
「ログイン処理」のような画面から独立したビジネスロジックは分離して別ファイルで管理するのが一般的だと思います。今一つVue.jsでそういったファイルを何処に配置するのがデファクトスタンダードなのか分からなかったのですが、いったんsrc直下に`modules`というディレクトリを作ってそこに`auth.js`を格納しました。ソースコードは以下の通り。

```src/modules/auth.js
import firebase from "firebase";
import axios from "axios";
import store from "@/store";

export default {
  init() {
    const config = {
      apiKey: process.env.VUE_APP_AUTH_API_KEY,
      authDomain: process.env.VUE_APP_AUTH_API_DOMAIN,
    };
    firebase.initializeApp(config);
  },
  loginWithTwitter() {
    this.login(new firebase.auth.TwitterAuthProvider());
  },
  loginWithGoogle() {
    this.login(new firebase.auth.GoogleAuthProvider());
  },
  login(provider) {
    const callApi = (token) => {
      const url = "http://localhost:8080/secured";
      const config = {
        headers: {
          Authorization: "Bearer " + token,
        },
      };
      axios.get(url, config).then((response) => {
          console.log(response);
      });
    };

    firebase
      .auth()
      .signInWithPopup(provider)
      .then((res) => {
        res.user
          .getIdToken()
          .then(function(){
             store.dispatch("user/store", {
              id: res.user.uid,
              token: token,
              name: res.user.displayName,
              pic: res.user.photoURL,
            });
            callApi(token);
           })
          .catch((error) => {
            console.log(error);
            this.errorMessage = error.message;
            this.showError = true;
          });
      });
  },
};
```

基本的には前回のコードを単にファイルに切り出しただけですが、`init`メソッドを定義してその中でconfigの初期化コードを入れました。これによって`main.js`にfirebaseのインポートが不要になり、別の実装に切り替える事などが容易になります。
また、Vuexを使って`user/store`にデータを格納しています。

Vueテンプレートや`main.js`の中で呼び出すときは下記のように普通にJavaScriptのモジュールとしてインポートして使います。

```src/views/Signin.vue
<template>
  <div class="signin">
    <h1>サインイン</h1>
    <button @click="signInWithGoogle">Googleで認証</button>
    <button @click="signInWithTwitter">Twitterで認証</button>
    <div v-show="$store.state.user.id">
      <p>ID: {{$store.state.user.id}}</p>
      <p>
        名前:
        <img :src="$store.state.user.pic" width="24px" height="24px" />
        {{$store.state.user.name}}
      </p>
      <p>有効期限: {{$store.state.user.expiration_time}}</p>
    </div>
  </div>
</template>

<script>
import Auth from "@/modules/auth";
export default {
  name: "Signin",
  data() {
    return {};
  },
  methods: {
    signInWithGoogle: function () {
      Auth.loginWithGoogle();
    },
    signInWithTwitter: function () {
      Auth.loginWithTwitter();
    },
  },
};
</script>
```

main.jsも`Auth.init()`を呼ぶ形に書き換えます。

```src/main.js
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import axios from "axios";
import VueAxios from "vue-axios";
import store from "@/store";
import Auth from "@/modules/auth";

Vue.config.productionTip = false;
Vue.use(VueAxios, axios);

Auth.init();

new Vue({
  router,
  render: (h) => h(App),
  store,
}).$mount("#app");
```

# Vue Routerで認可を行う

Vue Routerは特に変更はありません。

```src/router/index.js
import Vue from "vue";
import VueRouter from "vue-router";
import Index from "../views/Index.vue";
import Create from "../views/Create.vue";
import Signin from "../views/Signin.vue";

// store
import Store from "@/store";

Vue.use(VueRouter);

const routes = [
  {
    path: "/",
    name: "Index",
    component: Index,
    meta: { requiresAuth: true },
  },
  {
    path: "/create",
    name: "Create",
    component: Create,
    meta: { requiresAuth: true },
  },
  {
    path: "/signin",
    name: "Signin",
    component: Signin,
  },
];

const router = new VueRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes,
});

router.beforeEach((to, from, next) => {
  if (
    to.matched.some((record) => record.meta.requiresAuth) &&
    !Store.state.user.token
  ) {
    next({ path: "/signin", query: { redirect: to.fullPath } });
  } else {
    next();
  }
});

export default router;
```

# まとめ

とりあえずちゃんとした認証が出来たのでVue.jsである程度は作りたいものが作れるようになった気がします。