Search code examples
javascriptdesign-patternsvue.jsvuex

Pattern for redirecting on unauthorized vuex actions


Navigation guards are perfect for redirecting unauthorized users to a login page, but what does one do to redirect unauthorized vuex actions to a login page?

I can do this easily enough in the vue method where I'm calling the action like so:

  if (!this.isLoggedIn) {
    this.$router.push({ name: 'login', query: { comeBack: true } })
    $(`.modal`).modal('hide')
    return
  }

But then I'm inserting these 5 lines of code for every component method that requires authorization.

All the solutions I can think of sound hacky, so I'm wondering what the vuex way is:

  1. In order to reject it at the vuex action level, I have to pass up the $router instance, and I'm still reusing the 5 lines for each action that requires auth.

  2. I can handle this in a utility file, but then I'm handling $router instance in that file.

  3. I can use a global vue mixin and call it (a) before making a call and then again (b) when getting a 401 back from the server.

All those seem odd. What vuex way am I missing here?


Solution

  • This sounds like a job for middleware. Unfortunately, Vuex doesn't have an official way to do middleware.

    There is a subscribeAction() but that runs after the commit, so does not allow mods to the action. There is also a proposal Middleware processing between actions and mutation.

    As I see it, we want middleware to be able to do two generic things

    • cancel the action
    • allow alternative actions to be called

    The second is difficult to do without patching store.dispatch() or messing with the private property _actions after store has been created.

    However, to guard an action as you describe, we only need to be able to cancel it.


    Here is a poor-man's middleware for the modules pattern for Vuex store which I prefer.

    store construction from modules

    export const store = new Vuex.Store({
      modules: {
        config,
        pages: applyMiddleware(pages),
        measures,
        user,
        loadStatus,
        search
      }
    })
    

    applyMiddleware

    const applyMiddleware = function(module) {
      if(module.middlewares) {
        Object.values(module.middlewares).forEach(middlewareFn => {
          Object.keys(module.actions).forEach(actionName => {
            const actionFn = module.actions[actionName]
            module.actions[actionName] = addMiddleware(actionName, actionFn, middlewareFn)
          });
        })
      }
      return module;
    }
    

    addMiddleware

    const addMiddleware = function(actionName, actionFn, middlewareFn) {
      return function(context, payload) {
        const resultFn = middlewareFn(actionFn)
        if(resultFn) {
          resultFn(context, payload)
        }
      }
    }
    

    defining middleware in the module

    const actions = {
      myAction: (context, payload) => {
        ...
        context.commit('THE_ACTION', payload)
        ...
      },
    }
    
    const middlewares = {
      checkAuthMiddleware: (action) => {
        return this.isLoggedIn 
          ? action // if logged-in run this action
          : null;  // otherwise cancel it
      }
    }
    
    export default {
      state,
      getters,
      mutations,
      actions,
      middlewares
    }
    

    This implementation has module-specific middleware functions, but you could also define them globally and apply to as many modules as applicable.