Vue3でバックエンドから取得した認証状態によってルーティングを制限する

## この記事でやること
- vue3で認証状態によってルーティングを制限
- JWT認証とrouterの制限を一致させる（？）

## この記事でやらないこと
- JWT認証の実装(調べたらいくらでもでそうなので)
- 環境構築(調べたらいくらでもでそうなので)
- storeやcomposableなどの詳しい解説(この辺調べる人は多分わかってると思うので)

# 環境
vue.js 3.5.13
hono.js 4.7.5

## 前提
- JWTを使って認証する
- JWTの保存はHttpOnly
- ログインAPIと認証APIはできている

## 本題
### 元々のルーティング
バックエンド作ってなかったのでログイン時storeに放り込んでました。
``` router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import Stock from '@/views/Stock.vue'
import StaffList from '@/views/StaffList.vue'
import Salary from '@/views/Salary.vue'
import Login from '@/views/Login.vue'
import { useAuthStore } from '@/stores/auth'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home,
      meta: { requiresAuth: true }
    },
    {
      path: '/stock',
      name: 'stockList',
      component: Stock,
      meta: { requiresAuth: true }
    },
    {
      path: '/staff',
      name: 'staffList',
      component: StaffList,
      meta: { requiresAuth: true }
    },
    {
      path: '/salary',
      name: 'salary',
      component: Salary,
      meta: { requiresAuth: true }
    },
    {
      path: '/login',
      name: 'login',
      component: Login
    },
  ]
})

router.beforeEach((to, from, next) => {
  const authStore = useAuthStore()
  if (to.meta.requiresAuth && !authStore.isAuthenticated) {
    next('/login')
  } else {
    next()
  }
})

export default router

```

### 変わったこと
バックエンドで認証系を作った

### 変更後
``` composable/useAuth.ts
import { ofetch } from 'ofetch';
import { useAuthStore } from '@/stores/auth';

export const useAuth = () => {
  const BASE_URL = import.meta.env.VITE_API_BASE_URL
  const authStore = useAuthStore()
  const login = async (user_name: string, password: string) => {
    try{
      const res = await ofetch(`${BASE_URL}/api/auth/login`, {
        method: 'POST',
        credentials: 'include',
        body: { user_name, password },
      })
      return res
    } catch (e) {
      console.log(e)
    }
  }

  const logout = async () => {
    try {
      await ofetch(`${BASE_URL}/api/auth/logout`, {
        method: 'POST',
        credentials: 'include',
      })
      authStore.setLoggedIn(false)
    } catch(e) {
      console.error(e)
    }
  }

  const fetchMe = async () => {
    try {
      const res = await ofetch(`${BASE_URL}/api/auth/me`, {
        method: 'GET',
        credentials: 'include',
      })
      authStore.setLoggedIn(true)
    } catch(e) {
      console.error(e)
      authStore.setLoggedIn(false)
    }
  }

  return {
    login,
    logout,
    fetchMe,
  }
}
```

``` router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import Stock from '@/views/Stock.vue'
import StaffList from '@/views/StaffList.vue'
import Salary from '@/views/Salary.vue'
import Login from '@/views/Login.vue'
import { useAuthStore } from '@/stores/auth'
+ import { useAuth } from '@/composable/useAuth'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home,
      meta: { requiresAuth: true }
    },
    {
      path: '/stock',
      name: 'stockList',
      component: Stock,
      meta: { requiresAuth: true }
    },
    {
      path: '/staff',
      name: 'staffList',
      component: StaffList,
      meta: { requiresAuth: true }
    },
    {
      path: '/salary',
      name: 'salary',
      component: Salary,
      meta: { requiresAuth: true }
    },
    {
      path: '/login',
      name: 'login',
      component: Login
    },
  ]
})

- router.beforeEach((to, from, next) => {
+ router.beforeEach(async (to, from, next) => {
  const authStore = useAuthStore() 
-  if (to.meta.requiresAuth && !authStore.isAuthenticated) {
-    next('/login')
-  } else {
-    next()
-  }
+  const auth = useAuth()
+  if (to.meta.requiresAuth){
+    await auth.fetchMe()
+    if (!authStore.isLoggedIn) {
+      next('/login')
+    }
+  }
+  return next()
})

export default router

```


## 解説
useAuth.tsは認証系のモノをまとめたcomposableです。storeで保持しているログインしてるか？情報をログインや認証、ログアウトのタイミングで変えて、それをbeforeEachで確認してログインへ行くようにしてます。

ページ遷移ごとに /me を確認するのは冗長かもしれませんが私は一旦これでやってみます。
セキュリティ的には効果あると思います。


## 最後に
まだ実運用していないのでもしかしたらバグがでるかもです。出てしまった人や潜在バグ？に気づいた方は教えてください。責任は取れませんが解決法みつけて記事直します。

## 最終的なソース


``` composable/useAuth.ts
import { ofetch } from 'ofetch';
import { useAuthStore } from '@/stores/auth';

export const useAuth = () => {
  const BASE_URL = import.meta.env.VITE_API_BASE_URL
  const authStore = useAuthStore()
  const login = async (user_name: string, password: string) => {
    try{
      const res = await ofetch(`${BASE_URL}/api/auth/login`, {
        method: 'POST',
        credentials: 'include',
        body: { user_name, password },
      })
      return res
    } catch (e) {
      console.log(e)
    }
  }

  const logout = async () => {
    try {
      await ofetch(`${BASE_URL}/api/auth/logout`, {
        method: 'POST',
        credentials: 'include',
      })
      authStore.setLoggedIn(false)
    } catch(e) {
      console.error(e)
    }
  }

  const fetchMe = async () => {
    try {
      const res = await ofetch(`${BASE_URL}/api/auth/me`, {
        method: 'GET',
        credentials: 'include',
      })
      authStore.setLoggedIn(true)
    } catch(e) {
      console.error(e)
      authStore.setLoggedIn(false)
    }
  }

  return {
    login,
    logout,
    fetchMe,
  }
}
```

``` router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import Stock from '@/views/Stock.vue'
import StaffList from '@/views/StaffList.vue'
import Salary from '@/views/Salary.vue'
import Login from '@/views/Login.vue'
import { useAuthStore } from '@/stores/auth'
import { useAuth } from '@/composable/useAuth'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home,
      meta: { requiresAuth: true }
    },
    {
      path: '/stock',
      name: 'stockList',
      component: Stock,
      meta: { requiresAuth: true }
    },
    {
      path: '/staff',
      name: 'staffList',
      component: StaffList,
      meta: { requiresAuth: true }
    },
    {
      path: '/salary',
      name: 'salary',
      component: Salary,
      meta: { requiresAuth: true }
    },
    {
      path: '/login',
      name: 'login',
      component: Login
    },
  ]
})

router.beforeEach(async (to, from, next) => {
  const auth = useAuth()
  const authStore = useAuthStore()
  if (to.meta.requiresAuth){
    await auth.fetchMe()
    if (!authStore.isLoggedIn) {
      return next('/login')
    }
  }
  return next()
})

export default router

```