Rails APIモード + devise_token_auth + Vue.js 3 で認証機能付きのSPAを作る(Navigation Guard編)

# はじめに

本記事はAPIをRailsのAPIモードで開発し、フロント側をVue.js 3で開発して、認証基盤にdevise_token_authを用いてトークンベースの認証機能付きのSPAを作るチュートリアルのVue.js編の記事(Navigation Guard編)になります。

前回: [RailsAPIモード + devise_token_auth + Vue.js 3で認証機能付きのSPAを作る(Vue.js編その2)](https://qiita.com/kk-icare/items/b3aca446c24e89e018c4)

次回: [Rails APIモード + devise_token_auth + Vue.js 3 で認証機能付きのSPAを作る(ファイル投稿編)](https://qiita.com/kk-icare/items/50d440b3994234cb76db)

# NavigationGuardとは？

前回までで作成したアプリは、ログイン済みでもログイン画面のURLにアクセスすればアクセスできてしまいますし、ログインが必要なページにログインせずにアクセスできてしまいます。(401エラーが返るので投稿は表示されませんが。)

そういったケースで、

- ログイン済みの場合にログインページにアクセスが来たら別ページへリダイレクト
- ログインが必要なページに未ログインの状態でアクセスが来たらログインページにリダイレクト

させる処理が欲しくなってきます。

ここでNavigationGuardの出番です。

NavigationGuardは一言でいうと「ページ遷移時に任意の処理を挟むための仕組み」かと思います。

Vue.jsのVueRouterはNavigationGuardをサポートしていますので、その仕組みを大いに活用していきたいと思います。

## src/router/index.tsにmeta情報を追記する

Vueのルーティングにはmeta情報を付与することができます。
このmeta情報を元に、処理の分岐を行うことができます。

https://router.vuejs.org/ja/guide/advanced/meta.html

認証が必要なURLにはrequiresAuthを付与し、

```javascript
meta: { requiresAuth: true } 
```

認証が不要なURLにはrequiresNotAuthを付与します。

```javascript
meta: { requiresNotAuth: true } 
```

では、src/router/index.tsを編集していきます。

```javascript
  {
    path: '/login',
    name: 'Login',
    component: Login,
    meta: { requiresNotAuth: true } // 追加
  },
  {
    path: '/posts',
    name: 'Post',
    component: Post,
    meta: { requiresAuth: true } // 追加
  },
  {
    path: '/posts/new',
    name: 'NewPost',
    component: NewPost,
    meta: { requiresAuth: true } // 追加
  }
```

## authorizeToken関数の実装

次に、認証が必要なページにリクエストが来た場合に実行する処理を実装していきます。

以下のコマンドを実行してください。

```terminal
$ touch src/router/authGuard.ts
```

作成されたファイルを以下のように修正します。

```typescript
import { validateToken } from '@/api/auth'
import { NavigationGuardNext, RouteLocationNormalized, RouteRecordNormalized } from 'vue-router'

export const authorizeToken = (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
  if (to.matched.some((record: RouteRecordNormalized) => record.meta.requiresAuth)) {
    validateToken()
      .then(() => {
        next()
      })
      .catch(() => {
        next({ path: '/login' })
      })
  } else if (to.matched.some((record: RouteRecordNormalized) => record.meta.requiresNotAuth)) {
    validateToken()
      .then(() => {
        next({ path: '/posts' })
      })
      .catch(() => {
        next()
      })
  }
}

```

validateToken関数は後ほど定義しますが、クライアントから送信された認証Tokenの有効性を検証するAPIを叩く関数です。

vue-routerからimportしているのはauthorizeToken関数の引数であるtoとnextに型を当てるためです。

型を当てることで、VSCodeを使っているとマウスオーバーすることで補完が効くようになります。

![navigation-guard1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/884609/4d011390-6d6b-c64f-79d3-de608d8061b5.png)

to.matchedとするとRouteRecordNormalizedの配列が返ります。

RouteRecordNormalized型を確認してみます。

```typescript
export declare interface RouteRecordNormalized {
  // 省略
  /**
   * {@inheritDoc _RouteRecordBase.meta}
   */
  meta: Exclude<_RouteRecordBase['meta'], void>;

  // 省略
```

metaプロパティが定義されていることがわかりました。

後はjavaScriptのsome関数でmatches配列の中のRecordの中でmetaがrequiresAuthのものがあれば、という条件とrequiresNotAuthのものがあれば、という条件で分岐しています。

## validateToken関数の実装

次にvalidateToken関数を実装していきます。

src/api/auth.tsを編集します。

```typescript
export const logout = async () => {
  return await Client.delete('/auth/sign_out', { headers: getAuthDataFromStorage() })
    .then(() => {
      removeAuthDataFromStorage()
    })
}

// ここから追加

export const validateToken = async () => {
  return await Client.get('/auth/validate_token', { headers: getAuthDataFromStorage() })
    .then((response) => {
      setAuthDataFromResponse(response.headers)
      return response.data
    })
}

// ここまで
```

GETリクエストでAPI側の認証Tokenを検証するURLを叩いて、通信に成功したらLocalStorageの情報を更新する関数をコールして、レスポンスのデータを返却する処理を実装しています。

最後にindex.tsの中でrouterに対してbeforeEachでauthorizeToken関数をコールする処理を追加します。

```javascript
import NewPost from '@/views/NewPost.vue'
import { authorizeToken } from './authGuard' // 追加


// 省略

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})
router.beforeEach(authorizeToken) // 追加
```

## 動作確認

これで準備完了です。APIサーバーを立ち上げて、vue側のサーバーも立ち上げて動作確認をしてみます。
また、試しにtoオブジェクトの中身を見るためにconsole.logを仕込んでみましょう。

```typescript
export const authorizeToken = (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
  console.log(to) // 追加
  if (to.matched.some((record: RouteRecordNormalized) => record.meta.requiresAuth)) {
    validateToken()
```

localhost:8080/posts にアクセスしてみます。

![navigation-guard2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/884609/a2414622-9aaf-5732-6126-4144d4e94184.png)

Networkタブで通信内容の確認をすると、正しくvalidate_tokenのリクエストを送信できていることがわかります。

![navigation-guars3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/884609/4a2a5f98-84d8-e61a-1c9c-d0df78810316.png)

また、きちんとmeta情報としてルーティングにrequiresAuthが付与されています。

一度ログアウトをして動作を確認してみます。ログアウトボタンは今後実装予定のためLocalStorageの値を空にします。
ApplicationタブのLocal Storage配下のURLの上にマウスオーバーし、右クリックすると「clear」という項目が出ますのでそちらをクリックすると空にできます。

もう一度 localhost:8080/postsにアクセスします。

![navigation-guard4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/884609/27e2066c-850e-2b5d-49b1-951b94e8b622.png)

LocalStorageにTokenが保管されておらず、validate_tokenのリクエストが401だったため、Loginページにリダイレクトしています。

再度ログインして、 localhost:8080/login にアクセスを試みても、/postsにリダイレクトすることも確認するといいですね。


# まとめ

これで最低限のNavigationGuardの仕組みは実装できました。

今はログイン済みかどうかのみの判定ですが、今後、「管理者かどうか」等の新たなルールで判別することもあるかもしれません。

その時はまた別のguard関数を定義してやりましょう。

次回は画像投稿および表示の機能を実装したいと思います。