Nuxt3で@sidebase/nuxt-authのlocalの挙動を調べてみた で決めた方針(APIリクエストは自前でして、@sidebase/nuxt-authの状態管理のみ使用する)に従って対応して行きます。
前回:Nuxt BridgeをNuxt3に移行。injectを書き換える。utilsとの違い

nuxt-authを導入

現段階では、0.6.0-beta.4 0.6.1が入りました。

% yarn add -D @sidebase/nuxt-auth

nuxt.config.ts

  modules: [
    '@nuxtjs/i18n',
+    '@sidebase/nuxt-auth'

+  auth: {
+    provider: {
+      type: 'local'
+    }
+  },

pluginで$authを作る

injectを作っておいた方が、テストが書く時に状態を作りやすい事が解ったので作成する事にしました。呼び出し元の修正も少なくて済むので、お得ですね。

plugins/auth.ts

export default defineNuxtPlugin((_nuxtApp) => {
  const { status: authStatus, data: authData } = useAuthState()
  return {
    provide: {
      auth: reactive({
        loggedIn: computed(() => authStatus.value === 'authenticated'),
        data: computed(() => authData.value),
        user: computed(() => authData.value?.user),
        setData: (data: object) => { (authData.value as any) = data },
        setUser: (user: object) => { (authData.value as any) = { ...authData.value, user } },
        resetUserInfomationUnreadCount: () => { (authData.value as any).user.infomation_unread_count = 0 }
      })
    }
  }
})

reactiveにしないと”.value”が必要になり、呼び出し元の修正が増えます。
resetUserInfomationUnreadCountはお知らせの未読数をリセットする為に追加しています。

ログイン処理: loginWith()

ログインページ

this.$auth.loginWith() の部分です。
ログインAPIにリクエストして、ログイン状態にして元のページに遷移するように変更します。

例: pages/users/sign_in.vue

export default {

-      await this.$auth.loginWith('local', {
-        data: {
+      const [response, data] = await useApiRequest(this.$config.public.apiBaseURL + this.$config.public.authSignInURL, 'POST', {
        ...this.query,
        unlock_redirect_url: this.$config.public.frontBaseURL + this.$config.public.unlockRedirectUrl
      })

      if (response?.ok) {
        if (this.appCheckResponse(data, { toasted: true })) {
+          this.$auth.setData(data)
          this.appSetToastedMessage(data, false)
+
+          const { redirectUrl, updateRedirectUrl } = useAuthRedirect()
+          navigateTo(redirectUrl.value || this.$config.public.authRedirectHomeURL)
+          updateRedirectUrl(null)
+        }
-      } else {
-        if (!this.appCheckErrorResponse(response?.status, data, { toasted: true })) { return }
+      } else if (this.appCheckErrorResponse(response?.status, data, { toasted: true })) {

useAuthState()のdataにレスポンス(ユーザー情報)をセット(useStateが呼ばれる)すればログイン状態になる。
ログイン後のリダイレクトはloginWithがやってくれていましたが、自前になるので、useStateで保存しておいたURLまたはトップページにnavigateTo()で遷移させます。

this.$router.push()やreplace()は、navigateTo()を使う事が推奨されています。
Based on History API – [和訳] Nuxt3 公式サイト~useRouter

※VitestとVue Test Utilsを導入で、バグが見つかったので、修正しました。
AssertionError: expected true to be false(バグ)

作成: composables/authRedirect.ts

リダイレクト先の保存や取得をここにまとめています。

// ログイン後のリダイレク先を保存
export const useAuthRedirect = () => {
  const redirect = useState<string | null>('auth:redirect', () => null)
  const update = (path: string | null) => { redirect.value = path }
  return {
    redirectUrl: readonly(redirect),
    updateRedirectUrl: update
  }
}

リダイレクト処理: redirect()

this.$auth.redirect() の部分です。認証必須ページにアクセスした時に、ログインページに遷移。
ここで現在のURLを保存しておいて、ログイン後に元のページに戻るようにしています。
上で作成したuseAuthRedirectを使います。

例: utils/application.js

-      this.$auth.redirect('login') // NOTE: ログイン後、元のページに戻す
+      const { updateRedirectUrl } = useAuthRedirect()
+      updateRedirectUrl(this.$route?.fullPath)
+      navigateTo(this.$config.public.authRedirectSignInURL)

リロードや再表示時にログイン状態を復元: (middleware)

auth Moduleがやってくれていた部分です。

middlewareはページで呼び出した場合か、xxx.global.tsだと、初回と遷移する度に呼び出される。
pluginsは初回で、xxx.client.tsとxxx.server.tsで、クライアントとサーバーで処理を分けられるので、こちらを使います。

作成: plugins/authUser.client.ts

// リロードや再表示時にログイン状態に戻す
export default defineNuxtPlugin(async (_nuxtApp) => {
  const $config = useRuntimeConfig()
  /* c8 ignore next */ // eslint-disable-next-line no-console
  if ($config.public.debug) { console.log('authUser') }

  // Devise Token Auth
  if (localStorage.getItem('token-type') !== 'Bearer' || localStorage.getItem('access-token') == null) {
    /* c8 ignore next */ // eslint-disable-next-line no-console
    if ($config.public.debug) { console.log('...Skip') }
    return
  }

  const [response] = await useAuthUser()
  if (!response?.ok && response?.status === 401) { useAuthSignOut(true) }
})

ローカルストレージにtokenが保存されていたら、APIリクエストしてユーザー情報を取得します。
APIリクエスト以降は他でも使うので、useAuthUserを作成します。

作成: composables/authUser.ts

// ユーザー情報更新
export const useAuthUser = async () => {
  const $config = useRuntimeConfig()
  /* c8 ignore next */ // eslint-disable-next-line no-console
  if ($config.public.debug) { console.log('useAuthUser') }

  const [response, data] = await useApiRequest($config.public.apiBaseURL + $config.public.authUserURL)

  if (response?.ok) {
    const { data: authData } = useAuthState()
    authData.value = data
  }

  return [response, data]
}

元の宣言削除、自前でリダイレクト

pages/spaces/update/[code].vue

-  middleware: 'auth',

  async created () {
-    if (!this.$auth.loggedIn) { return } // NOTE: Jestでmiddlewareが実行されない為
+    if (!this.$auth.loggedIn) { return this.appRedirectAuth() }

ログイン状態確認、ユーザー情報取得: loggedIn/user

情報が更新されたら新しい状態や値を表示したい場合

this.$auth.loggedInやthis.$auth.userの部分です。
暫定対応した「$auth?.loggedIn」を「$auth.loggedIn」に戻せばOK。

例: layouts/default.vue

-      <template v-if="!$auth.loggedIn">
+-      <template v-if="!$auth?.loggedIn">
+      <template v-if="!$auth.loggedIn">

Composition APIでは、リアクティブな値は、valueプロパティでアクセスする必要がある。
リアクティブ:値が監視されていて、その値が更新された時に変更が検知される状態。
(まだ理解が浅い。。。)

遷移時の情報で状態や値を表示したい場合

ボタン押下で、ログアウト状態にするので、エラーにならないようにする為です。

例: pages/users/delete.vue

-          アカウントは{{ $auth.user.destroy_schedule_days || 'N/A' }}日後に削除されます。それまでは取り消し可能です。<br>
+          アカウントは{{ destroyScheduleDays || 'N/A' }}日後に削除されます。それまでは取り消し可能です。
<script> export default { data () { return { loading: true, processing: true, + destroyScheduleDays: this.$auth.user?.destroy_schedule_days

ユーザー情報更新: fetchUser()

this.$auth.fetchUser() の部分です。APIリクエストして最新のユーザー情報取得します。
上で作成したuseAuthUserを使います。

例: pages/users/update.vue

    // ユーザー情報更新 // NOTE: 最新の状態が削除予約済みか確認する為
-    try {
-      await this.$auth.fetchUser()
+    const [response, data] = await useAuthUser()
-    } catch (error) {
+    if (!response?.ok) {
-      return this.appCheckErrorResponse(null, error, { redirect: true, require: true }, { auth: true })
+      return this.appCheckErrorResponse(response?.status, data, { redirect: true, require: true }, { auth: true })
    }

ログアウト処理: logout()

this.$auth.logout() の部分です。
APIリクエストやローカルストレージの削除は、useAuthSignOutを作成してまとめます。

例: utils/application.js

-          this.appSignOut()
+          useAuthSignOut(true)
+          this.appRedirectAuth()

-    // ログアウト
-    async appSignOut (message = 'auth.unauthenticated', path = null, data = null) {
-      try {
-        await this.$auth.logout()
-      } catch (error) {
-        this.appCheckErrorResponse(null, error, { toasted: true, require: true })
-      }
-
-      // 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')
-      }
-
-      if (message != null) {
-        this.$toasted.info(this.$t(message)) // NOTE: メッセージを上書き
-      }
-      if (path != null) {
-        this.$router.push({ path, query: { alert: data.alert, notice: data.notice } })
-      }
-    }

例: pages/users/sign_out.vue

-      this.appSignOut('auth.signed_out')
+      await useAuthSignOut()
+      this.appSetToastedMessage({ notice: this.$t('auth.signed_out') }, false)
+      navigateTo(this.$config.public.authRedirectLogOutURL)

例: pages/users/delete.vue

-        this.appSignOut(null, '/users/sign_in', data)
+        await useAuthSignOut()
+        navigateTo({ path: this.$config.public.authRedirectSignInURL, query: { alert: data.alert, notice: data.notice } })

作成: composables/authSignOut.ts

// ログアウト
export const useAuthSignOut = async (skipRequest = false) => {
  const $config = useRuntimeConfig()
  /* c8 ignore next */ // eslint-disable-next-line no-console
  if ($config.public.debug) { console.log('useAuthSignOut') }

  if (!skipRequest) {
    await useApiRequest($config.public.apiBaseURL + $config.public.authSignOutURL, 'POST')
  }

  // Devise Token Auth
  if (localStorage.getItem('token-type') === 'Bearer') {
    const reqAccessToken = localStorage.getItem('access-token')
    if (reqAccessToken != null) {
      localStorage.removeItem('token-type')
      localStorage.removeItem('uid')
      localStorage.removeItem('client')
      localStorage.removeItem('access-token')
      localStorage.removeItem('expiry')
    }
  }

  const { data: authData } = useAuthState()
  authData.value = null
}

アラート対応

onUnmounted is called when there is no active component instance to be associated with.(↑で対応済み)

[Vue warn]: onUnmounted is called when there is no active component instance to be
 associated with. Lifecycle injection APIs can only be used during execution of setup().
 If you are using async setup(),
 make sure to register lifecycle hooks before the first await statement.

原因の箇所

useAuthState.mjs

  const _rawTokenCookie = useCookie("auth:token",
    { default: () => null, maxAge: config.token.maxAgeInSeconds,
      sameSite: config.token.sameSiteAttribute });

今回は使ってないけど、useCookieは、
Lifecycle injection APIで、setup()の中でのみ使用できると書いてあります。

Options APIで書いてますが、computedの中で定義していたのが原因のようです。
dataやcreatedの中でも問題ないですが、揃えてexportの前に移動して解消しています。
また、methodsの中で定義してもwarnは表示されなかった。

<script>
+ const { status: authStatus, data: authData } = useAuthState()
+
export default {
  computed: {
    loggedIn () {
-      const { status } = useAuthState()
-      return status.value === 'authenticated'
+      return authStatus.value === 'authenticated'
    },
    authData () {
-      const { data } = useAuthState()
-      return data.value
+      return authData.value
    },

Cannot read properties of undefined (reading ‘info’)

Uncaught (in promise) TypeError: Cannot read properties of undefined (reading ‘info’)

暫定対応(置換)

「$toasted.」->「$toasted?.」

TODO: 後で、toastedを入れる時に対応してから書き換えます。
Nuxt BridgeをNuxt3に移行。vue-toast-notificationかvue-toastificationを導入

No match found for location with path

[Vue Router warn]: No match found for location with path “/infomations/14”

ファイル名のルールが変わっているのでリネームします。

pages/infomations/_id.vue -> [id].vue

v-on with no argument expects an object value. -> #activator

[Vue warn]: v-on with no argument expects an object value.

#activator(v-btnやv-list-groupなど)の仕様が変わっているので、変更します。

例: layouts/default.vue

-          <template #activator="{ on, attrs }">
+          <template #activator="{ props }">
            <v-btn
+              v-bind="props"
              id="user_menu_btn"
              class="d-inline-block"
              max-width="400px"
              text
-              v-bind="attrs"
-              v-on="on"
            >

序でに動かないので修正します。

          <v-list-group>
-            <template #activator>
+            <template #activator="{ props }">
+              <v-list-item v-bind="props">
              <v-avatar size="32px">
                <v-img id="user_image" :src="$auth.user.value.image_url.small" />
              </v-avatar>
              <v-list-item-title>
                <div class="text-truncate ml-1">{{ $auth.user.value.name }}</div>
              </v-list-item-title>
+               </v-list-item>
           </template>

tooltipも動かないので修正します。シンプルに書けるようなってますね。
序でに「width >= 960」を「mdAndUp」に変更。

-        <v-tooltip bottom :disabled="$vuetify.breakpoint.width >= 960">
-          <template #activator="{ on: tooltip }">
-            <v-icon small v-on="tooltip">mdi-download</v-icon>
+        <v-icon small>mdi-download</v-icon>
         <span class="hidden-sm-and-down ml-1">ダウンロード</span>
-          </template>
-          ダウンロード
-        </v-tooltip>
+        <v-tooltip activator="parent" location="bottom" :disabled="$vuetify.display.mdAndUp">ダウンロード</v-tooltip>

更に序でに、mdの最大が1264pxから1280pxに変更になっています。
デフォルトのmobileBreakpointがlgで、mobileで判定できるので変更。
【Vuetify3】Display — Vuetify
【Vuetify2】Display helpers — Vuetify

  created () {
-    this.drawer = this.$vuetify.display.width >= 1264 // NOTE: md(Medium)以下の初期表示はメニューを閉じる
+    this.drawer = !this.$vuetify.display.mobile

v-dialogが表示されない -> #default

アラートは出てないですが、閉じるボタンが動かないので修正します。

例: pages/users/delete.vue

        <v-dialog transition="dialog-top-transition" max-width="600px">

-          <template #default="dialog">
+          <template #default="{ isActive }">

                <v-btn
                  id="user_delete_no_btn"
                  color="secondary"
-                  @click="dialog.value = false"
+                  @click="isActive.value = false"
                >
                  いいえ(キャンセル)
                </v-btn>
                <v-btn
                  id="user_delete_yes_btn"
                  color="error"
-                  @click="postUserDelete(dialog)"
+                  @click="postUserDelete(isActive)"
                >
                  はい(削除)
                </v-btn>

-    async postUserDelete ($dialog) {
+    async postUserDelete (isActive) {
      this.processing = true
-      $dialog.value = false
+      isActive.value = false

アラートは出てないですが、ページネーションのボタンが動かないので修正します。

例: pages/infomations/index.vue

              <v-pagination
                id="pagination1"
                v-model="page"
                :length="infomation.total_pages"
-                @input="getInfomationsList()"
+                @click="getInfomationsList()"
              />

次回は暫定対応したtoast(通知が一定時間表示されて消えるやつ)に対応します。
Nuxt BridgeをNuxt3に移行。vue-toast-notificationかvue-toastificationを導入

今回のコミット内容

origin#507 nuxt-authを導入
origin#507 pluginで$authを作る
origin#507 リロード時にtokenが無効だったら次回APIリクエストされないようにする

Nuxt BridgeをNuxt3に移行。nuxt-authを導入” に対して2件のコメントがあります。

コメントを残す

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