feathersjs-ecosystem / feathers-vuex

Integration of FeathersJS, Vue, and Nuxt for the artisan developer
https://vuex.feathersjs.com
MIT License
445 stars 108 forks source link

Nuxt auth plugin not initiating after page refresh #435

Open mategvo opened 4 years ago

mategvo commented 4 years ago

Steps to reproduce

(First please check that this issue is not already solved as described here)

Expected behavior

If user is signed in, a page refresh should keep them logged in.

Actual behavior

User is not authenticated, regardless of jwt-token being stored in localStorage or cookies

System configuration

Latest nuxt js and feathers vuex Node feathers API

nuxt.config.js:

  modules: [
    // Doc: https://bootstrap-vue.js.org
    'bootstrap-vue/nuxt',
    // Doc: https://axios.nuxtjs.org/usage
    '@nuxtjs/axios',
    '@nuxtjs/pwa',
    // Doc: https://github.com/nuxt-community/dotenv-module
    '@nuxtjs/dotenv',
    '@nuxtjs/toast',
    // '@nuxtjs/auth'
    'nuxt-client-init-module'
  ],

store/index.js (as per official documentation)

// ~/store/index.js
import {
  makeAuthPlugin,
  initAuth,
  hydrateApi,
  models
} from '~/plugins/feathers'
const auth = makeAuthPlugin({
  userService: 'users',
  state: {
    publicPages: [
      'login',
      'signup',
      'index',
      'terms-of-service',
      'privacy-policy'
    ]
  },
  actions: {
    onInitAuth({ state, dispatch }, payload) {
      if (payload) {
        dispatch('authenticate', {
          strategy: 'jwt',
          accessToken: state.accessToken
        })
          .then((result) => {
            // handle success like a boss
            console.log('loged in')
          })
          .catch((error) => {
            // handle error like a boss
            console.log(error)
          })
      }
    }
  }
})

const requireModule = require.context(
  // The path where the service modules live
  './services',
  // Whether to look in subfolders
  false,
  // Only include .js files (prevents duplicate imports`)
  /.js$/
)
const servicePlugins = requireModule
  .keys()
  .map((modulePath) => requireModule(modulePath).default)

export const modules = {
  // Custom modules
}

export const state = () => ({
  // Custom state
})

export const mutations = {
  // Custom mutations
}

export const actions = {
  // Custom actions
  nuxtServerInit({ commit, dispatch }, { req }) {
    return initAuth({
      commit,
      dispatch,
      req,
      moduleName: 'auth',
      cookieName: 'feathers-jwt'
    })
  },
  nuxtClientInit({ state, dispatch, commit }, context) {
    hydrateApi({ api: models.api })

    if (state.auth.accessToken) {
      return dispatch('auth/onInitAuth', state.auth.payload)
    }
  }
}

export const getters = {
  // Custom getters
}

export const plugins = [...servicePlugins, auth]

Module versions (especially the part that's not working):

"@feathersjs/authentication-client": "^4.5.1",
"@feathersjs/configuration": "^2.0.6",
"@feathersjs/express": "^1.3.1",
"@feathersjs/feathers": "^3.3.1",
"@feathersjs/rest-client": "^4.5.1",
"@feathersjs/socketio-client": "^4.5.1",
"@nuxtjs/axios": "^5.9.5",
"@nuxtjs/dotenv": "^1.4.0",
"@nuxtjs/pwa": "^3.0.0-0",
"@nuxtjs/toast": "^3.3.0",
"@vue/composition-api": "^0.4.0",
"bootstrap": "^4.1.3",
"bootstrap-vue": "^2.0.0",
"consola": "^2.11.3",
"cookie-storage": "^6.0.0",
"cross-env": "^5.2.0",
"feathers-hooks-common": "^5.0.2",
"feathers-vuex": "^3.6.1",
"nuxt": "^2.0.0",
"nuxt-client-init-module": "^0.1.8",
"socket.io-client": "^2.3.0"

NodeJS version: v12.15.0

Operating System:

macos & docker

Browser Version: Latest Chrome

React Native Version: no react

Module Loader: NPM

mategvo commented 4 years ago

However picks this up, many thanks in advance for support. We are currenlty solving the problem in default.vue template

  async created() {
    await this.authenticate()
      .then(() => {
        // todo - redirect to where the user came from
        this.$router.push('/events')
      })
      .catch((e) => {
        console.log('not authenticated')
        this.logout()
      })
  },

but we are now having problems with redirects.

marssantoso commented 4 years ago

Could this be related to this issue?

itoonx commented 4 years ago

Same problem with the lastest version of feathers-vuex and nuxt SSR

mategvo commented 4 years ago

I've created a plugin to solve this problem. It works with localstorage. Needs to be edited slighltly for cookie storage

// ~/plugins/authInit.js
const storedToken = typeof localStorage['feathers-jwt'] !== 'undefined'
const hashTokenAvailable = window.location.hash.indexOf('access_token' > -1)

export default async (context) => {
  if (
    (!context.app.store.state.auth.user && storedToken) ||
    hashTokenAvailable
  ) {
    console.log('Authenticating', context.app.store.state.auth.user)
    await context.app.store
      .dispatch('auth/authenticate')
      .then(() => {
        console.log('Authenticated', context.app.store.state.auth.user)
      })
      .catch((e) => {
        console.error(e)
      })
  }
}

Remember to initialize it in nuxt.config.js

// nuxt.config.js
  plugins: [
    { src: '~/plugins/authInit.js', ssr: false }
  ],

Here's the version for cookies-storage

// ~/plugins/authInit.js
import { CookieStorage } from 'cookie-storage'

const cookieStorage = new CookieStorage()
const cookie = cookieStorage.getItem('feathers-jwt') !== null
const hashTokenAvailable = window.location.hash.indexOf('access_token' > -1)

export default async (context) => {
  if ((!context.app.store.state.auth.user && cookie) || hashTokenAvailable) {
    console.log('Authenticating', context.app.store.state.auth.user)
    await context.app.store
      .dispatch('auth/authenticate')
      .then(() => {
        console.log('Authenticated', context.app.store.state.auth.user)
      })
      .catch((e) => {
        console.error(e)
      })
  }
}

Hope this helps

marshallswain commented 4 years ago

@mateuszgwozdz could you find some time to add your solution to what you see as the best place for it in the Feathers-Vuex Nuxt docs? I've been doing more and more work with Gridsome's pre-rendering, so I'm a little out of the loop with Nuxt. No worries if not. I will leave this open.

mategvo commented 4 years ago

Sure, I will do it with pleasure. I just thought that something is not working as supposed to for me, rather than it's a missing functionality

mategvo commented 4 years ago

I put this in the docs in a quite early place, related to auth. It's becuase I assumed this functionality is included and actually lost significant amount of time trying to "fix" it, find a bug. I think it will prevent others from misunderstanding this feature. Still I believe this is something that should work out-of-the-box. I can also write the plugin in the way that automatically determines whether we are using localstorage or cookie and restores the session - would that be useful as out-of-the-box feature?

https://github.com/feathersjs-ecosystem/feathers-vuex/compare/master...mateuszgwozdz:patch-1

andrewharvey commented 4 years ago

@mateuszgwozdz Unfortunately that patch doesn't work for me with using https://vuex.feathersjs.com/nuxt.html#full-nuxt-configuration-example.

andrewharvey commented 4 years ago

Interestingly though I can only replicate this with nuxt (dev mode) not when running on production with nuxt build && nuxt start.

mategvo commented 4 years ago

Doesn't work for you with SSR you mean? My project is a simple SPA, I forgot to mention I haven't tested SSR, I don't think it will work

mategvo commented 4 years ago

I can confirm it works for me fine in nuxt start. I reload live website and I am still authenticated

andrewharvey commented 4 years ago

Doesn't work for you with SSR you mean? My project is a simple SPA, I forgot to mention I haven't tested SSR, I don't think it will work

That's right I'm using 'universal' mode with Nuxt, but only when in dev mode not production do I see the issue anyway.

ghost commented 3 years ago

Hey guys I wanted to share a workaround that worked for me to get authenticated server side whereas @mategvo's solution will only work client side which can cause problems.

The root of the problem comes from the fact that cookie-storage requires document to work properly which will not work server side to parse the feathers-jwt token.

Compounding that fact is that plugins/feathers from the full nuxt example has .configure(auth({ storage })) which isn't yet aware of the req variable required to parse cookies server side so I cooked up a workaround. Bear in mind that this isn't fully tested yet so there could be some gotchas further down the road so if you see one let me know.

First off, replace cookie-storage with universal-cookie

Then give it a decorate it with the required methods

class CookieStorage extends Cookies {
  get getItem() { return this.get; }
  get setItem() { return this.set; }
  get removeItem()  { return this.remove; }
}

Then, install cookie-universal-nuxt and refactor @mategvo 's code so that it will work on the server side too.

Here's my full workaround:

// ~/plugins/authInit.js
import { storage } from './feathers';

const hashTokenAvailable = process.client && window.location.hash.indexOf('access_token' > -1);

export default async ({ store, $cookie }) => {
  // Give the cookie to the auth module's storage instance
  const cookie = $cookie.get('feathers-jwt');
  if (cookie) {
    storage.set('feathers-jwt', cookie);
  }

  if ((!store.state.auth.user && cookie) || hashTokenAvailable) {
    await store
      .dispatch('auth/authenticate')
      .then(() => {
        console.log('Authenticated');
      })
      .catch((e) => {
        console.error(e)
      })
  }
}
// ~/plugins/feathers.js
import feathers from '@feathersjs/feathers'
import rest from '@feathersjs/rest-client'
import axios from 'axios'
import socketio from '@feathersjs/socketio-client'
import auth from '@feathersjs/authentication-client'
import io from 'socket.io-client'
import Cookies from 'universal-cookie';
import { iff, discard } from 'feathers-hooks-common'
import feathersVuex, { initAuth, hydrateApi } from 'feathers-vuex'

const apiUrl = process.env.API_URL;

let socket
let restClient
// We won't use socket to comunicate from server to server
if (process.client) {
  socket = io(apiUrl, { transports: ['websocket'] })
} else {
  restClient = rest(apiUrl)
}
const transport = process.client ? socketio(socket) : restClient.axios(axios)

class CookieStorage extends Cookies {
  get getItem() { return this.get; }
  get setItem() { return this.set; }
  get removeItem()  { return this.remove; }
  // and any other required method as needed
}
const storage = new CookieStorage()

const feathersClient = feathers()
  .configure(transport)
  .configure(auth({ storage }))
  .hooks({
    before: {
      all: [
        iff(
          context => ['create', 'update', 'patch'].includes(context.method),
          discard('__id', '__isTemp')
        )
      ]
    }
  })

export default feathersClient

// Setting up feathers-vuex
const { makeServicePlugin, makeAuthPlugin, BaseModel, models, FeathersVuex } = feathersVuex(
  feathersClient,
  {
    serverAlias: 'api', // optional for working with multiple APIs (this is the default value)
    idField: '_id', // Must match the id field in your database table/collection
    whitelist: ['$regex', '$options'],
    enableEvents: process.client // Prevent memory leak
  }
)

export {
  makeAuthPlugin,
  makeServicePlugin,
  BaseModel,
  models,
  FeathersVuex,
  initAuth,
  hydrateApi,
  storage,
}

Obviously not the best solution here; hoping this SSR oversight gets fixed later.