Nuxt.jsにnuxt/authを導入して、導入したDevise Token Auth向けにテスト(RSpec)を書くで作成したRailsアプリと連携して認証(ログイン・ログアウト)を実装してみました。

authとauth-nextの違い

Status – nuxt auth docs

@nuxtjs/auth-next (v5)
@nuxtjs/auth (v4)

バージョンの違いかな。安定版はv4っぽい。
auth-nextも試しましたが動かなかったので、今回はauthを採用。

nuxt/auth導入

Nuxt.jsでVue.jsに触れてみる で作成したアプリに導入します。

$ yarn add @nuxtjs/auth

package.jsonとyarn.lockが更新される。

nuxt.config.js に追加

  modules: [
    // https://go.nuxtjs.dev/axios
    '@nuxtjs/axios',
+    // https://auth.nuxtjs.org/
+    '@nuxtjs/auth',
    // https://go.nuxtjs.dev/pwa
    '@nuxtjs/pwa'
  ],

tsconfig.json に追加(今回は使わないけど、いずれTypeScriptに書き換えたいので入れておく)

    "types": [
      "@nuxt/types",
      "@nuxtjs/axios",
+      "@nuxtjs/auth",
      "@types/node"
    ]

store/index.js を作成
※空ファイルで良い

nuxt/authの設定

nuxt.config.js に追加

export default {
+  server: {
+    port: 5000
+  },

ポートがRailsアプリと被る。先にRailsを起動して、Nuxt動かせばポート変わるけど、逆だとエラーになるし、RailsのURLを設定するので、序でに変更しておく。

  auth: {
    redirect: {
      login: '/login',
      logout: '/login',
      callback: false,
      home: '/'
    },
    strategies: {
      local: {
        token: {
          property: 'token',
          global: true
        },
        user: {
          property: 'user'
        },
        endpoints: {
          login: { url: 'http://localhost:3000/users/auth/sign_in.json', method: 'post' },
          logout: { url: 'http://localhost:3000/users/auth/sign_out.json', method: 'delete' },
          user: { url: 'http://localhost:3000/users/auth/validate_token.json', method: 'get' }
        }
      }
    }
  },

endpointsは、RailsアプリのDevise Token AuthのURLを設定

ログインページ作成

pages/login.vue を作成

<template>
  <v-card max-width="480px">
    <v-form>
      <v-card-title>
        ログイン
      </v-card-title>
      <v-card-text>
        <v-text-field v-model="email" label="メールアドレス" />
        <v-text-field v-model="password" type="password" label="パスワード" />
        <v-btn color="primary" @click="login">
          ログイン
        </v-btn>
      </v-card-text>
    </v-form>
  </v-card>
</template>

<script>
export default {
  middleware: 'auth', // TODO: トップページでalert表示「既にログインしています。」

  data () {
    return {
      email: '',
      password: ''
    }
  },

  methods: {
    async login () {
      await this.$auth.loginWith('local', {
        data: {
          email: this.email,
          password: this.password
        }
      })
        .then((response) => {
          if (response.data.alert) { console.log('[OK]alert: ' + response.data.alert) } // TODO: 遷移元ページでalert表示
          if (response.data.alert) { console.log('[OK]notice: ' + response.data.notice) } // TODO: 遷移元ページでnotice表示
          return response
        },
        (error) => {
          if (typeof error.response === 'undefined') {
            console.log('[ERROR]' + error) // TODO: alert表示
          } else {
            if (error.response.data.alert) { console.log('[NG]alert: ' + error.response.data.alert) } // TODO: alert表示
            if (error.response.data.notice) { console.log('[NG]notice: ' + error.response.data.notice) } // TODO: notice表示
          }
          return error
        })
    }
  }
}
</script>

通信エラーやレスポンスをメッセージ表示させたいけど、今はログに出力して、TODOにしておく。
→ 対応しました。コミットログ参照
https://dev.azure.com/nightonly/nuxt-app-origin/_git/nuxt-app-origin/commit/8d6b7cb6c61470697cc9b5bc4a479aaa84e8b3a2

ログアウトページ作成

アクションだけでも良いですが、確認した方が間違えてログアウトするのを避けられそうなので、ページにしてみました。ネットバックとかで良く見かける。

pages/logout.vue を作成

<template>
  <v-card max-width="480px">
    <v-card-title>
      ログアウトします。よろしいですか?
    </v-card-title>
    <v-card-text>
      <v-btn to="/" nuxt>
        トップページ
      </v-btn>
      <v-btn color="primary" @click="logout">
        ログアウト
      </v-btn>
    </v-card-text>
  </v-card>
</template>

<script>
export default {
  middleware: 'auth', // TODO: ログインページでnotice表示「既にログアウト済みです。」

  methods: {
    logout () {
      this.$auth.logout() // TODO: ログインページでnotice表示「ログアウトしました。」
    }
  }
}
</script>

最初、ログインと同じようにレスポンスで制御しようとしましたが、responseがundefinedになりました。logoutWithもなさそう。
そもそも通信に失敗してもフロントはログアウト状態になるので、ハンドリングしても意味がない。
よくよく考えると、サーバーサイドのtoken消すよりも、ユーザーの状態変更を優先した方が良い。
いずれ有効期限切れるし、デフォルト最大10件なので、古いのから追い出されるし。

Devise Token Auth対応

Devise Token Authでは、ヘッダーのuid/client/access-tokenを認証に使うので、このままだとログイン状態を維持できない。→ Devise Token Authの挙動を確認してみた
sign_in.jsonの直後にvalidate_token.jsonが呼ばれ、ヘッダーにuid/client/access-tokenが無いのでtokenチェックに失敗する。

下記を参考に対応
I need the token(s) to be fetched from the header instead of data of the request. · Issue #76 · nuxt-community/auth-module · GitHub

nuxt.config.js に追加

  plugins: [
+    { src: '~/plugins/axios.js' }
  ],

plugins/axios.js を作成

export default function ({ $axios }) {
  $axios.onRequest((config) => {
    // Devise Token Auth
    if (localStorage.getItem('token-type') === 'Bearer' && localStorage.getItem('access-token')) {
      config.headers.uid = localStorage.getItem('uid')
      config.headers.client = localStorage.getItem('client')
      config.headers['access-token'] = localStorage.getItem('access-token')
    }
  })

  $axios.onResponse((response) => {
    // Devise Token Auth
    if (response.headers['token-type'] === 'Bearer' && response.headers['access-token']) {
      localStorage.setItem('token-type', response.headers['token-type'])
      localStorage.setItem('uid', response.headers.uid)
      localStorage.setItem('client', response.headers.client)
      localStorage.setItem('access-token', response.headers['access-token'])
      localStorage.setItem('expiry', response.headers.expiry)
    }
  })
}

Devise Token Auth以外では無視されるようにtoken-typeとaccess-tokenの有無でチェック。
一定時間内のリクエスト(batch_request)はaccess-tokenとexpiryに半角スペース(フロントでは空)になるので、onResponseではaccess-tokenがある場合のみローカルストレージに保存。

クリーニング

消さなくても動くけど、下記でログアウト時にローカルストレージから削除する処理を追加。

pages/logout.vue に追加

-    logout () {
+    async logout () {
-      this.$auth.logout() // TODO: ログインページでnotice表示「ログアウトしました。」
+      await this.$auth.logout() // TODO: ログインページでnotice表示「ログアウトしました。」
+      // Devise Token Auth
+      if (localStorage.getItem('token-type') === 'Bearer' && localStorage.getItem('access-token')) {
+        localStorage.removeItem('token-type')
+        localStorage.removeItem('uid')
+        localStorage.removeItem('client')
+        localStorage.removeItem('access-token')
+        localStorage.removeItem('expiry')
      }
    }

async/awaitにしないと、先にローカルストレージから削除され、sign_out.jsonが失敗します。

動作確認

ログインするとトップページに遷移します。
OPTIONS(preflight)リクエストも正常に返却されています。
こちら設定済み → CORS設定でOPTIONSリクエストとヘッダ取得できない問題に対応する

動線を作る

サイドメニューにログイン・ログアウトリンクを設置します。

layouts/default.vue を変更

      <v-list>
        <v-list-item
          v-for="(item, i) in items"
+          v-if="(item.loggedIn == null) || (item.loggedIn == $auth.loggedIn)"
          :key="i"
          :to="item.to"
          router
          exact
        >
<script>
export default {
  data () {
    return {
      clipped: false,
      drawer: false,
      fixed: false,
      items: [
        {
          icon: 'mdi-apps',
          title: 'Welcome',
-          to: '/'
+          to: '/',
+          loggedIn: null
        },
        {
          icon: 'mdi-chart-bubble',
          title: 'Inspire',
-          to: '/inspire'
+          to: '/inspire',
+          loggedIn: null
+        },
+        {
+          icon: 'mdi-login',
+          title: 'ログイン',
+          to: '/login',
+          loggedIn: false
+        },
+        {
+          icon: 'mdi-logout',
+          title: 'ログアウト',
+          to: '/logout',
+          loggedIn: true
        }
      ],

リファクタリング

これでも動くがlint掛けるとerrorになる。

$ yarn lint
  13:11  error  The 'items' variable inside 'v-for' directive should be
 replaced with a computed property that returns filtered array instead.
 You should not mix 'v-for' with 'v-if'  vue/no-use-v-if-with-v-for

v-forとv-ifは一緒に使わないでと。優先度の問題らしい。
【Vue.js】`v-for`, `v-if` を一緒に使うのは避けよう – Qiita

メッセージにあるcomputed property(算出プロパティ)を使ってしました。

      <v-list>
        <v-list-item
-          v-for="(item, i) in items"
+          v-for="(item, i) in displayItems"
-          v-if="(item.loggedIn == null) || (item.loggedIn == $auth.loggedIn)"
          :key="i"
          :to="item.to"
          router
          exact
        >
+  },
+
+  computed: {
+    displayItems () {
+      return this.items.filter(item => (item.loggedIn == null) || (item.loggedIn === this.$auth.loggedIn))
+    }
  }
}
</script>

今回のコミット内容
https://dev.azure.com/nightonly/nuxt-app-origin/_git/nuxt-app-origin/commit/b408c690b21c300d35dd21e1f7bff9b81ec32a9a

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です