Yura
Yura

Reputation: 2248

Nuxt 3, unable to find the error: A composable that requires access to the Nuxt instance was called outside of a plugin

I have a composable that returns a function to verify auth and refresh token within an interval of 5 seconds. Both utilizing Nuxt's useCookie utility function to perform the logic behind it.

At first, it works. But then sometimes, when the token expired, it broke and throw me an error shown on the error screen. This is the error:


500

[nuxt] A composable that requires access to the Nuxt instance was called outside of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function. This is probably not a Nuxt bug. Find out more at `https://nuxt.com/docs/guide/concepts/auto-imports#using-vue-and-nuxt-composables`.

at Module.useAsyncData (./node_modules/nuxt/dist/app/composables/asyncData.js:26:38)
at useFetch (./node_modules/nuxt/dist/app/composables/fetch.js:53:43)
at refreshToken (./composables/useRepository.ts:206:35)
at verifyAuth (./composables/useRepository.ts:250:31)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async setup (./layouts/dashboard.js:35:87)

I've read my code implementation many times but cannot find where the problem is. Please help me find the error.

This is the piece of codes that causing this error to be happen.

useRepository.ts

export const useRepository = () => {
  const config = useRuntimeConfig()
  const cookie = useCookie<LoginResponse | null>('auth')

  const refreshToken = async () => {
    const { data, error } = await useFetch<RefreshTokenResponse>(
      `/api/jwt-auth/jwt/refresh/`,
      {
        baseURL: config.public.apiUrl,
        method: 'POST',
        query: { format: 'json' },
        body: { refresh: cookie.value?.refresh },
      },
    )

    if (error.value) {
      const { status } = error.value
      if (status === 400 || status === 401 || status === 403) {
        cookie.value = null
        navigateTo('/login?redirect=session-expired')
        return false
      }
    }

    if (!cookie.value) {
      console.warn(
        'You have issued a refresh token but no users has signed in to this session, check your code implementation.',
      )
      return false
    }

    if (!data.value || !data.value.access) {
      console.warn(
        'Server returns no access token, users will be forcefully kicked out to the login page. Please talk to your server administrator.',
      )
      return false
    }

    cookie.value = { ...cookie.value, access: data.value.access }
    return true
  }

  const verifyAuth = async (timer?: ReturnType<typeof setInterval>) => {
    const { error, execute: refetch } = await useFetch<VerifyAuthResponse>(
      `/api/jwt-auth/jwt/verify/`,
      {
        baseURL: config.public.apiUrl,
        method: 'POST',
        query: { format: 'json' },
        body: { token: cookie.value?.access },
      },
    )

    if (error.value) {
      const data = await error.value.data
      if (data && data.code === 'token_not_valid') {
        const success = await refreshToken()
        if (!success) {
          if (timer) {
            console.log('clearing auth cookie')
            clearInterval(timer)
          }
          cookie.value = null
          return navigateTo('/login?redirect=session-expired')
        } else {
          console.log('refetching')
          await refetch()
        }
      }
    }
  }

  return {
    verifyAuth,
    refreshToken,
  }
}

dashboard.vue

<script setup lang="ts">
useHead({
  htmlAttrs: {
    lang: 'id',
  },
  titleTemplate(title) {
    return title
      ? `${title} - MyApp`
      : 'My Branding Punchline - MyApp'
  },
  bodyAttrs: {
    class:
      'scrollbar scrollbar-thumb-primary scrollbar-track-primary/10 scrollbar-thin scroll-smooth',
  },
})

const { verifyAuth } = useRepository()
await verifyAuth()

const timer = ref<ReturnType<typeof setInterval>>()
const stopVerifyingAuth = () => {
  if (timer.value) clearInterval(timer.value)
}

const continuouslyVerifyAuth = () => {
  timer.value = setInterval(() => {
    verifyAuth(timer.value)
  }, 5000)
}

onActivated(() => continuouslyVerifyAuth())
onDeactivated(() => stopVerifyingAuth())
onUnmounted(() => stopVerifyingAuth())

const windowFocus = useWindowFocus()
watch(windowFocus, async (isFocus) => {
  if (isFocus) {
    await verifyAuth(timer.value)
    continuouslyVerifyAuth()
  } else {
    stopVerifyingAuth()
  }
})
</script>

<template>
  <main class="box-border antialiased text-black overflow-x-hidden">
    <div class="h-screen flex items-start">
      <TheSidebar />
      <div
        class="w-full h-full overflow-y-auto scrollbar-thumb-primary scrollbar-track-primary/10 scrollbar-thin scroll-smooth"
      >
        <slot />
      </div>
    </div>
  </main>
</template>

Upvotes: 2

Views: 1808

Answers (1)

Yura
Yura

Reputation: 2248

After days of debugging, it turns out that my implementation should not be put inside the layouts file.

I don't know why.

My current solution is to create a wrapper component to be used on each page.

So now my code look like this

layouts/dashboard.vue

<script setup lang="ts">
useHead({
  htmlAttrs: {
    lang: 'id',
  },
  titleTemplate(title) {
    return title
      ? `${title} - MyApp`
      : 'My Branding Punchline - MyApp'
  },
  bodyAttrs: {
    class:
      'scrollbar scrollbar-thumb-primary scrollbar-track-primary/10 scrollbar-thin scroll-smooth',
  },
})
</script>

<template>
  <main class="box-border antialiased text-black overflow-x-hidden">
    <div class="h-screen flex items-start">
      <TheSidebar />
      <div
        class="w-full h-full overflow-y-auto scrollbar-thumb-primary scrollbar-track-primary/10 scrollbar-thin scroll-smooth"
      >
        <slot />
      </div>
    </div>
  </main>
</template>

components/DashboardContent.vue

<script setup lang="ts">
const { verifyAuth } = useRepository()
await verifyAuth()

const timer = ref<ReturnType<typeof setInterval>>()
const stopVerifyingAuth = () => {
  if (timer.value) clearInterval(timer.value)
}

const continuouslyVerifyAuth = () => {
  timer.value = setInterval(() => {
    verifyAuth(timer.value)
  }, 5000)
}

onActivated(() => continuouslyVerifyAuth())
onDeactivated(() => stopVerifyingAuth())
onUnmounted(() => stopVerifyingAuth())

const windowFocus = useWindowFocus()
watch(windowFocus, async (isFocus) => {
  if (isFocus) {
    await verifyAuth(timer.value)
    continuouslyVerifyAuth()
  } else {
    stopVerifyingAuth()
  }
})
</script>

<template>
  <div>
    <slot />
  </div>
</template>

pages/dashboard/index.vue

<script setup lang="ts">
  definePageMeta({ middleware: ['auth'], layout: 'dashboard' })
</script>

<template>
  <DashboardContent>
    Hello World!
  </DashboardContent>
</template>

Upvotes: 0

Related Questions