nuxt icon indicating copy to clipboard operation
nuxt copied to clipboard

SSR & Await (directus example)

Open 53RG1005 opened this issue 2 years ago • 10 comments

Environment


  • Operating System: Darwin
  • Node Version: v16.17.1
  • Nuxt Version: 3.1.0
  • Nitro Version: 2.0.0
  • Package Manager: [email protected]
  • Builder: vite
  • User Config: -
  • Runtime Modules: -
  • Build Modules: -

Reproduction

(https://github.com/bryantgillespie/nuxt3-directus-starter) Create directus plugin

import { BaseStorage, Directus } from '@directus/sdk'
import { useAuth } from '~~/store/auth'

// Make sure you review the Directus SDK documentation for more information
// https://docs.directus.io/reference/sdk.html

export default defineNuxtPlugin(async (nuxtApp) => {
  const { directusUrl } = useRuntimeConfig()

  // Create a new storage class to use with the SDK
  // Needed for the SSR to play nice with the SDK
  class CookieStorage extends BaseStorage {
    deletedKeys = new Set<string>()
    get(key: string) {
      if (this.deletedKeys.has(key)) return null
      const cookie = useCookie(key)
      return cookie.value
    }
    set(key: string, value: string) {
      this.deletedKeys.delete(key)
      const cookie = useCookie(key)
      return (cookie.value = value)
    }
    delete(key: string) {
      this.deletedKeys.add(key)
      const cookie = useCookie(key)
      return (cookie.value = null)
    }
  }

  // Create a new instance of the SDK
  const directus = new Directus(directusUrl, {
    storage: new CookieStorage(),
    auth: {
      mode: 'json',
    },
  })

  // Inject the SDK into the Nuxt app
  nuxtApp.provide('directus', directus)

  // We're calling the useAuth composable here because we need to define Directus as a plugin first
  const auth = useAuth()

  const token = await directus.auth.token
  const side = process.server ? 'server' : 'client'

  // If there's a token but we don't have a user, fetch the user
  if (!auth.isLoggedIn && token) {
    console.log('Token found, fetching user from ' + side)
    console.log('Token is', token)
    try {
      await auth.getUser()
      console.log('User fetched succeessfully from ' + side)
    } catch (e) {
      console.log('Failed to fetch user from ' + side, e.message)
    }
  }

  // If the user is logged in but there's no token, reset the auth store {
  if (auth.isLoggedIn && !token) {
    console.log('Token not found, resetting auth store from ' + side)
    auth.$reset()
  }
})

Create page with

<script setup>
const { $directus } = useNuxtApp()
const usr = await $directus.users.me.read()
</script>

Describe the bug

When SSR enabled:

400
Bad Request

at createError (./node_modules/h3/dist/index.mjs:48:15)
at Object.handler (./.nuxt/dev/index.mjs:740:15)
at Object.handler (./node_modules/h3/dist/index.mjs:723:31)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async toNodeHandle (./node_modules/h3/dist/index.mjs:798:7)
at async Object.ufetch [as localFetch] (./node_modules/unenv/runtime/fetch/index.mjs:9:17)
at async Object.errorhandler [as onError] (./.nuxt/dev/index.mjs:453:30)
at async Server.toNodeHandle (./node_modules/h3/dist/index.mjs:805:9)

When SSR disabled: it works fine!

Additional context

No response

Logs

Only 400 - bad request

53RG1005 avatar Jan 26 '23 22:01 53RG1005

When i used try - catch it start work, but in console i have

TransportError: nuxt instance unavailable at Transport.request /.output/server/node_modules/@directus/sdk/dist/sdk.cjs.js:730:19) at runMicrotasks () at processTicksAndRejections (node:internal/process/task_queues:96:5) .....

parent: Error: nuxt instance unavailable at useNuxtApp /.output/server/chunks/app/server.mjs:152:13)

53RG1005 avatar Jan 27 '23 09:01 53RG1005

https://stackblitz.com/edit/github-cfogyu

53rg0 avatar Jan 27 '23 12:01 53rg0

Hey,

Just out of curiosity, why are you not using the directus module for nuxt?

Baroshem avatar Jan 27 '23 14:01 Baroshem

Hey,

Just out of curiosity, why are you not using the directus module for nuxt?

I tried this plugin, but with all due respect to the work, it does not deserve attention

I create custom endpoints using directus and i want to use it in nuxt Problem is in any sdk where we have async function and setup as plugin

53RG1005 avatar Jan 27 '23 14:01 53RG1005

What about wrapping your directus call with a onMounted or inside useAsyncData like following?

<template><div>test</div></template>
<script setup>
const { $directus } = useNuxtApp();
console.log($directus);
onMounted(async () => {
  const usr = await $directus.users.me.read();
  console.log(usr);
});
</script>

In my case it seems to be working correctly and the issue with the instance is gone

Baroshem avatar Jan 27 '23 14:01 Baroshem

What about wrapping your directus call with a onMounted or inside useAsyncData like following?

<template><div>test</div></template>
<script setup>
const { $directus } = useNuxtApp();
console.log($directus);
onMounted(async () => {
  const usr = await $directus.users.me.read();
  console.log(usr);
});
</script>

In my case it seems to be working correctly and the issue with the instance is gone

It's not SRR, it's just a simple example of my request, in real project based on data should build whole page and it should be with ssr. When we use onMounted -> its client side rendering

53RG1005 avatar Jan 27 '23 14:01 53RG1005

Hmm, it seems that it does not work on either top lever await nor on useAsyncData.

Baroshem avatar Jan 27 '23 15:01 Baroshem

When I replaced your call for geting users with just a simple console log to see what is stored as a $directus in nuxtApp, the error is no more and I can see the following code in the console:

$directus Directus {                                                                                                                                          16:09:44
  _url: undefined,
  _options: {
    storage: CookieStorage { prefix: '', deletedKeys: Set(0) {} },
    auth: { mode: 'json' }
  },
  _items: {},
  _singletons: {},
  _storage: CookieStorage { prefix: '', deletedKeys: Set(0) {} },
  _transport: Transport {
    config: { url: undefined, beforeRequest: [AsyncFunction: beforeRequest] },
    axios: [Function: wrap] {
      request: [Function: wrap],
      getUri: [Function: wrap],
      delete: [Function: wrap],
      get: [Function: wrap],
      head: [Function: wrap],
      options: [Function: wrap],
      post: [Function: wrap],
      postForm: [Function: wrap],
      put: [Function: wrap],
      putForm: [Function: wrap],
      patch: [Function: wrap],
      patchForm: [Function: wrap],
      defaults: [Object],
      interceptors: [Object],
      create: [Function: create]
    },
    beforeRequest: [AsyncFunction: beforeRequest]                                                                                                             16:09:34
  },
  _auth: Auth {
    mode: 'json',
    autoRefresh: true,
    msRefreshBeforeExpires: 30000,
    staticToken: '',
    _transport: Transport {
      config: [Object],
      axios: [Function],
      beforeRequest: [AsyncFunction: beforeRequest]
    },
    _storage: CookieStorage { prefix: '', deletedKeys: Set(0) {} }
  }
}

Are you sure you should be accessing this directus client like this const usr = await $directus.users.me.read();?

The error with the nuxt instance seems to be completely unrelated to this as this is $directus variable is just a client, it does not have users in it.

Unless I am doing something wrong.

Baroshem avatar Jan 27 '23 15:01 Baroshem

I have the same issue:

- Operating System: `Windows_NT`
- Node Version:     `v18.13.0`
- Nuxt Version:     `3.1.1`
- Nitro Version:    `2.1.0`
- Package Manager:  `[email protected]`
- Builder:          `vite`
- User Config:      `ssr`, `modules`, `strapi`
- Runtime Modules:  `@nuxtjs/[email protected]` <- not active in my example
- Build Modules:    `-`

With the following code I am able to reproduce the issue, and all I am changing (and have) in the Nuxt config is ssr. With ssr: false the code works, with ssr: true the code does not work on first load, but it does work if I make changes to the code and it hot reloads.

<template>
  <div>
    <h1>useAsyncData</h1>
    <pre>
      {{ data }}
    </pre>
    <h1>useFetch</h1>
    <pre>
      {{ data2 }}
    </pre>
  </div>
</template>

<script setup>
const { data } = await useAsyncData('test', () => useFetch('http://localhost:1337/api/articles'));
const data2 = await useFetch('http://localhost:1337/api/articles');
</script>

The browser output:

useAsyncData
      {
  "data": null,
  "pending": false,
  "error": {
    "name": "FetchError"
  },
  "execute": {},
  "refresh": {}
}
    
useFetch
      {
  "data": null,
  "pending": false,
  "error": "Error: fetch failed ()"
}
    

The api is Strapi, and I have the same issue if I use the Strapi module.

niklasfjeldberg avatar Jan 29 '23 20:01 niklasfjeldberg

It was a lot of fiddling and trying things out... for below to work you have to be on the same domain and have ssl enabled. It basically always comes down to the cookie settings.

Directus: api.domain.com App: domain.com Cookie: .domain.com

Dropping my working plugins/directus.js and composables. (sorry for the long paste) below code only puts the refresh token in a cookie, all other short lived token info is going in the state.

This is far from perfect, but better than using the directus module, which somehow does not work for the more advanced usecases.

import { BaseStorage, Directus } from '@directus/sdk'
import { useCookie } from '#imports'

export default defineNuxtPlugin(async (nuxtApp) => {
  const { directusUrl, cookieDomain } = useRuntimeConfig()

  const auth = useAuth()
  const user = useUser()

  const auth_refresh_token = useCookie('directus_refresh_token', {
    maxAge: 3600 * 24 * 7,
    // httpOnly: true,
    secure: true,
    sameSite: 'None',
    domain: cookieDomain, // .maindomain.com (put directuson a subdomain like api.maindomain.com)
  })

  const auth_token = useState('auth_token')
  const auth_expires = useState('auth_expires')
  const auth_expires_at = useState('auth_expires_at')

  const cookies = {
    auth_token: auth_token,
    auth_expires: auth_expires,
    auth_expires_at: auth_expires_at,
    auth_refresh_token: auth_refresh_token,
  }

  class CookieStorage extends BaseStorage {
    deletedKeys = new Set()
    get(key) {
      if (this.deletedKeys.has(key)) return null
      const cookie = cookies[key]

      return cookie.value
    }
    set(key, value) {
      this.deletedKeys.delete(key)
      const cookie = cookies[key]

      return (cookie.value = value)
    }
    delete(key) {
      this.deletedKeys.add(key)
      const cookie = cookies[key]
      return (cookie.value = null)
    }
  }

  // Create a new instance of the SDK
  const directus = new Directus(directusUrl, {
    storage: new CookieStorage(),
    auth: {
      mode: 'json',
    },
  })

  // Inject the SDK into the Nuxt app
  nuxtApp.provide('directus', directus)

  let token = await directus.auth.token
  // console.log(token)
  if (process.server && auth_refresh_token.value && !token) {
    try {
      const x = await directus.auth.refresh()
    } catch (e) {
      //failed
      console.log(e, auth_refresh_token.value)
    }
  }

  const side = process.server ? 'server' : 'client'
  token = await directus.auth.token
  if (auth.isAuthenticated.value === false && token !== undefined) {
    try {
      if (process.server) {
        await user.init(directus)
        auth.isAuthenticated.value = true
      }
    } catch (e) {
      console.log(e)
      // failed?
    }
  }

  // If the user is logged in but there's no token, reset the auth store {
  if (auth.isAuthenticated.value && !token) {
    console.error('Token not found, resetting auth store from ' + side)
  }
})

composables/useUser.js

export const useUser = () => {
  const { $directus } = useNuxtApp()
  const user = useState('directus.user', () => ({}))

  const init = async (directus) => {
    const directusUser = directus ? directus.users : $directus.users
    const value = await directusUser.me.read({
      fields: ['*'],
    })
    user.value = value
  }

  return {
    state: user,
    init,
    setData: (value) => {
      user.value = value
    },
    data: computed(() => user.value),
    reset: () => (user.value = {}),
  }
}

composables/useAuth.js

export const useAuth = () => {
  const nuxtApp = useNuxtApp()
  const { $directus } = useNuxtApp()
  const route = useRoute()
  const userState = useUser()

  const isAuthenticated = useState('auth.isAuthenticated', () => false)
  const stateErrors = useState('auth.errors', () => [])

  const login = async (email, password) => {
    stateErrors.value = []
    await $directus.auth.login({
      email: email,
      password: password,
    })
    await $directus.auth.refresh()
    // fetch user data
    await userState.init()
    isAuthenticated.value = true
  }


  const loginOnSubmit = async ($event) => {
    const email = $event.srcElement.email.value
    const password = $event.srcElement.password.value
    try {
      await login(email, password)
      return true
    } catch (error) {
      if (error.response) {
        stateErrors.value = error.response.errors
        return error.response.errors
      }
      return false
    }
  }

  const logout = async () => {
    await $directus.auth.logout()
    isAuthenticated.value = false
    if (nuxtApp.ssrContext === undefined) {
      window.location = route.path
    }
  }

  return {
   isAuthenticated,
   login,
   logout
  }
}

I am currently looking into sidebase.io/nuxt-auth/ and https://gist.github.com/madsh93/b573b3d8f070e62eaebc5c53ae34e2cc ... just a small thing i need to figure out with refresh tokens not being updated before it's usable. The moment I got it working I wil create a Nuxt "layer". Also looking into using Directus SDK without axios by replacing it with $fetch or use https://github.com/jacoborus/directus-lite-sdk.

vanling avatar Jan 30 '23 15:01 vanling

I think the answer was given to the question. I am closing the issue but if there is still something that should be added, I will reopen the issue, no worries :)

Baroshem avatar Feb 01 '23 17:02 Baroshem