【Vue】メニュー周りの権限制御を行う

# 概要
VueなどでSPAを構築する場合、大体こんな感じのメニューがあると思います。
<img width="347" alt=" 2019-08-24 13.39.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/198781/10a5e1e6-6cd1-377b-9b7c-3a5b7427fd8d.png">


クリックされた項目でURLをルーティングして内容を変化させるやつですね。

## 権限制御を行う
このメニューで、以下のような権限制御を行う場合の例をご紹介します。
(※バックエンドについては今回省略します。)

- ユーザーのロールに設定された権限によりメニューの項目を出し分けたい
- 使用する権限を持っていない場合、項目は非表示とする
- URL直打ちでも開けないようにしたい

## 環境
- Vue 2.6
- VueRouter 3.0
- Vuex 3.0
- Vue Router Multiguard
    - VueRouterのbeforeEnternに使用可能なヘルパー関数を用意してくれます
    - URLごとにナビゲーションガードを行うために使用します
    - https://github.com/atanas-angelov-dev/vue-router-multiguard#readme

## テーブル
テーブルはこんな感じです
<img width="293" alt=" 2019-08-26 21.32.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/198781/01efa4ee-4d7c-4253-1367-9d805cecbd82.png">
ユーザーはロールを１つ持ち、ロールは権限を複数持つ構成となっています。

### ユーザーテーブルのレコード例

| ユーザーコード | パスワード | ロールコード |
| :---: | :---: | :---: |
| admin | ******* | admin |
| user1 | ******* | writer|
| user2 | ******* | readonly|

### ロール権限のレコード例

| ロールコード | 権限 | 
| :---: | :---: | 
| admin | menu1 | 
| admin | menu2 |
| admin | menu3 |
| writer | menu1 |
| writer | menu2 |
| readonly | menu1 |



## 権限を取得

### ストア
- ユーザーに紐付いた権限を保持します。

```javascript:action-types.js
export const GET_MENU_FLGS = "GET_MENU_FLGS";

export const ACTION = {
  GET_MENU_FLGS
};
```

```javascript:mutation-types.js
export const UPDATE = "UPDATE";

export const MUTATION = {
  UPDATE
};
```

```javascript:menu-store.js
import { MUTATION } from "./mutation-types";
import { ACTION } from "./action-types";
import axios from "axios";

const state = {
  //権限
  menuFlgs: {}
};

const getters = {
  menuFlgs: state => {
    return state.menuFlgs;
  }
};

const actions = {
  //ログインユーザーのメニュー使用権限を取得します。
  async [ACTION.GET_MENU_FLGS]({ commit }) {
    await axios
      .get("menus")
      .then(res => {
        commit(MUTATION.UPDATE, {
          menuFlgs: res.data
        });
      })
      .catch(err => {
        throw err;
      });
  }
};

const mutations = {
  [MUTATION.UPDATE](state, { menuFlgs }) {
    state.menuFlgs = menuFlgs;
  }
};

export default {
  state: state,
  getters: getters,
  actions: actions,
  mutations: mutations
};

```

権限のJSONはこのようなイメージです。

```javascript
menuFlgs: [
  {
    menu1: true
  },
  {
    menu2: true
  },
  {
    menu3: true
  }
];
```

### VueRouter
- ①URLごとに権限を持っているかを判定します
    - 権限がない場合は遷移をキャンセルします
- ②ストアに権限が保持されていない場合、取得します 


```javascript:router.js
import Vue from "vue";
import Router from "vue-router";
import multiguard from "vue-router-multiguard";
import store from "/store/store.js";
import { GET_MENU_FLGS } from "/store/menu/action-types";

Vue.use(Router);

/* *
 * ①ナビゲーションガード
 * */
const menu1Guard = (to, from, next) => {
  if (store.getters.menuFlgs.menu1) {
    next();
  } else {
    next(false);
  }
};

const menu2Guard = (to, from, next) => {
  if (store.getters.menuFlgs.menu2) {
    next();
  } else {
    next(false);
  }
};

const menu3Guard = (to, from, next) => {
  if (store.getters.menuFlgs.menu3) {
    next();
  } else {
    next(false);
  }
};

/* *
 * ルート定義
 * */
const router = new Router({
  routes: [
    {
      path: "/menu1",
      //①
      beforeEnter: multiguard([menu1Guard]),
      component: () => import("@/components/pages/menu1/menu1Template")
    },
    {
      path: "/menu2",
      //①
      beforeEnter: multiguard([menu2Guard]),
      component: () => import("@/components/pages/menu2/menu2Template")
    },
    {
      path: "/menu3",
      //ナビゲーションガードを複数設定することもできます。
      beforeEnter: multiguard([menu1Guard, menu2Guard, menu3Guard]),
      component: () => import("@/components/pages/menu3/menu3Template")
    }
  ]
});


/* *
 * ②権限を取得します。
 * */
router.beforeEach(async (to, from, next) => {
  if (!Object.keys(store.getters.menuFlgs).length) {
    await store.dispatch(GET_MENU_FLGS);
  }

  next();
});

export default router;


```


# メニュー表示
- ①メニューを配列で定義しておきます。
- ②ストアの権限から、使用可能なメニューのみ表示します

```vuejs
<template>
  <v-list>
    <v-list-tile v-for="menu in availableMenus" :key="menu.id">
      <router-link :to="menu.url">{{menu.title}}</router-link>
    </v-list-tile>
  </v-list>
</template>

<script>
export default {
  data() {
    return {
      //①メニューマスタ
      menuMaster: [
        {
          id: 1,
          title: "メニュー１",
          url: "/menu1"
        },
        {
          id: 2,
          title: "メニュー2",
          url: "/menu2"
        },
        {
          id: 3,
          title: "メニュー３",
          url: "/menu3"
        }
      ]
    };
  },
  computed: {
    //②使用可能なメニューで絞り込みます
    availableMenus() {
      let available = [];
      for (const menu of this.menuMaster) {
        if (this.isAvailable(menu.url)) {
          available.push(menu);
        }
      }
      return available;
    }
  },
  methods: {
    //ログインユーザーがメニューの使用権限を持っているかを判定します。
    isAvailable(url) {
      const menuName = url.slice(1);
      return this.$store.getters.menuFlgs[menuName];
    }
  }
};
</script>

```

# まとめ

- Vue Router Multiguardを使うとナビゲーションガードが扱いやすくなりました
- 権限をどのタイミングで取得するかは賛否両論ありそうです