Quasar - SSR and using cookies
Tags: vue, ssr, quasar
Published: 2019-10-30 00:51:29

#### Getting Quasar’s SSR cookie plugin working with other libraries and services.

## Table Of Contents
  * [1 Introduction](#1-introduction)
  * [2 The Problem](#2-the-problem)
  * [3 The Solution](#3-solution)
  * [4 Vuex's Stores](#4-vuexs-stores)
  * [5 Navigation Guards](#5-navigation-guards)
  * [6 Other Services](#6-other-services)
  * [7 Simplified Injection](#7-simplified-injection)
  * [8 About Quasar](#8-about-quasar)

###1 - Introduction

If you've read the Quasar docs regarding the Cookies plugin, you probably also noticed a small note about how to use this plugin in a SSR app.

> When building for SSR, use only the $q.cookies form. If you need to use the import { Cookies } from 'quasar', then you’ll need to do it like this:
> 
> ```js
> import { Cookies } from 'quasar'
> 
> // you need access to `ssrContext`
> function (ssrContext) {
>   const cookies = process.env.SERVER
>     ? Cookies.parseSSR(ssrContext)
>     : Cookies // otherwise we're on client
> 
>   // "cookies" is equivalent to the global import as in non-SSR builds
> }
> ```
> 
> The ssrContext is available in App Plugins or preFetch feature where it is supplied as parameter.
> 
> The reason for this is that in a client-only app, every user will be using a fresh instance of the app in their browser. For server-side rendering we want the same: each request should have a fresh, isolated app instance so that there is no cross-request state pollution. So Cookies needs to be bound to each request separately.

Now let's imagine you're using axios with interceptors to consume your REST API, and you're configuring everything in a boot file like something similar to this:

**./src/boot/axios.js**
```js
import Vue from 'vue'
import axios from 'axios'

const axiosInstance = axios.create({
  baseURL: 'https://api.example.com'
})

axiosInstance.interceptors.request.use(config => {
  let token = localStorage.getItem("token")
  if (token) {
    config.headers.Authorization = `bearer ${token}`
  }
  return config;
}, error => {
  return Promise.reject(error)
})

Vue.prototype.$axios = axiosInstance

export { axiosInstance }
```

You're using this axios instance in order to consume a REST API which is behind an authorization wall and you're storing the token on the client's side only. In that case, if the user requests a route from the server, which needs to consume a protected resource, this request will fail, because the server won't have recieved the user's token.

One way to solve this problem is persist the token in a `Cookie` instead of the `localStorage`. 

**./src/boot/axios.js**
```js
import axios from 'axios'

const axiosInstance = axios.create({
  baseURL: 'https://api.example.com'
})

export default function ({ Vue, ssrContext }) {
  const cookies = process.env.SERVER
    ? Cookies.parseSSR(ssrContext)
    : Cookies

  axiosInstance.interceptors.request.use(config => {
    let token = cookies.get('token')
    if (token) {
      config.headers.Authorization = `bearer ${token}`
    }
    return config;
  }, error => {
    return Promise.reject(error)
  })

  Vue.prototype.$axios = axiosInstance
}

export { axiosInstance }
```

After doing this, you'll probably want to test the application locally. And, more than likely, the app will work flawlessly. So you'll continue to do some integrations tests, and there you'll have success. Now confident of your app's cookie system for authenticaion, you'll publish a new version of your app and it will work correctly in 99.9% of the requests.

But, for some strange reason, users will complain about a bug, where sometimes they see things from other users, which they actually shouldn't. We have a big security issue.

###2 - The Problem

You had only one instance of axios, which is shared between all requests, and each request will call the boot function and will register a new interceptor.

Since the interceptors are overriding the header, the app will use the token from the user who made the last request. Because of that, if two users make a request at the same time, both will use the same token. And even worse, an unauthorized user could get access to a protected route. In that case, the app will use the token from the last authorized user, who made a request and this is really, really bad.

###3 - The Solution

So, let's recapitulate the last line of the docs regarding the use of the Cookie Plugin in an SSR app.

> For server-side rendering we want the same: each request should have a fresh, isolated app instance so that there is no cross-request state pollution. So Cookies needs to be bound to each request separately.

Since the axios instance had the Cookie Plugin as a dependency, we'll noew need to bind a new axios instance to each request.

**./src/boot/axios.js**
```js
import Vue from 'vue'
import axios from 'axios'

Vue.mixin({
  beforeCreate () {
    const options = this.$options
    if (options.axios) {
      this.$axios = options.axios
    } else if (options.parent) {
      this.$axios = options.parent.$axios
    }
  }
})

export default function ({ app, ssrContext }) {
  let instance = axios.create({
    baseURL: 'https://api.example.com'
  })

  const cookies = process.env.SERVER
    ? Cookies.parseSSR(ssrContext)
    : Cookies

  instance.interceptors.request.use(config => {
    let token = cookies.get('token')
    if (token) {
      config.headers.Authorization = `bearer ${token}`
    }
    return config;
  }, error => {
    return Promise.reject(error)
  })

  app.axios = instance
}
```

With the above code, you can safely use the `$axios` instance in your components, but what about vuex's stores and navigation guards?

###4 - Vuex's Stores

The `scope` of the `mutations`, `actions` and `getters` of a vuex's store and your modules is the store itself. So, if we need to access the axios instance, we just need to append this to the store.

**./src/boot/axios.js**
```js
import Vue from 'vue'
import axios from 'axios'

Vue.mixin({/*...*/})

export default function ({ app, store, ssrContext }) {
  let instance = axios.create(/*...*/)

  // cookies and interceptors

  app.axios = instance
  store.$axios = instance
}
```

and furthermore in the store....

```
export default {
  namespaced: true,
  state () {
    return {
      field: ''
    }
  },
  mutations: {
    field (state, value) { state.field = value }
  },
  actions: {
    async doSomething({ commit }) {
      let { value } = await this.$axios.get('endpoint_url')
      commit('field', value)
    }
  }
}
```

###5 - Navigation Guards

Like Vuex's store, we'll need to append the axios instance to the router.

**./src/boot/axios.js**
```js
import Vue from 'vue'
import axios from 'axios'

Vue.mixin({/*...*/})

export default function ({ app, store, router, ssrContext }) {
  let instance = axios.create(/*...*/)

  // cookies and interceptors

  app.axios = instance
  store.$axios = instance
  router.$axios = instance
}
```

But, unfortunately the `router` isn't in the scope of the navigation guards, so we'll need to keep a reference to the `router` somewhere.

**./src/router/index.js**
```js
import Vue from 'vue'
import VueRouter from 'vue-router'

import routes from './routes'

Vue.use(VueRouter)

export default function (context) {
  context.router = new VueRouter({
    scrollBehavior: () => ({ x: 0, y: 0 }),
    routes: routes,
    mode: process.env.VUE_ROUTER_MODE,
    base: process.env.VUE_ROUTER_BASE
  })

  context.router.beforeEach((to, from, next) => {
    let { router, store } = context
    let { $axios } = router
    console.log(router, store , $axios)
    next()
  })
  return context.router
}
```

And what about the `per-route guards`? Well, we'll need to make a small change in the `./src/router/routes.js` that will no longer return an array of routes, but a function, which will receive the context as an argument and return an array of routes.

```js
export default function (context) {
  const routes = [
    {
      path: '/',
      component: () => import('layouts/MyLayout.vue'),
      children: [
        { path: '', component: () => import('pages/Index.vue') }
      ],
      beforeEnter (to, from, next) {
        let { router, store } = context
        let { $axios } = router
        console.log(router, store , $axios)
        next()
      }
    }
  ]
  // Always leave this as last one
  if (process.env.MODE !== 'ssr') {
    routes.push({
      path: '*',
      component: () => import('pages/Error404.vue')
    })
  }
  return routes
}
```

Of course, we'll need to update the `./src/router/index.js`.

```js
import Vue from 'vue'
import VueRouter from 'vue-router'

import routes from './routes'

Vue.use(VueRouter)

export default function (context) {
  context.router = new VueRouter({
    scrollBehavior: () => ({ x: 0, y: 0 }),
    routes: routes(context),
    mode: process.env.VUE_ROUTER_MODE,
    base: process.env.VUE_ROUTER_BASE
  })
  return context.router
}
```

###6 - Other services

Here, I have bad news, if you're using your axios instance in other services. You'll need to figure out a way to pass a reference of the axios to them, like this:

```js
class Service {
  axios = void 0
  cookies = void 0
  constructor (axios, ssrContext ) {
    this.cookies = process.env.SERVER
      ? Cookies.parseSSR(ssrContext)
      : Cookies
    this.axios = axios
  }
  async auth ({ username, password }) {
    let { data: token } = this.axios.post('auth_url', { username, password })
    this.cookies.set('token', token)
  }
}

export default function ({ app, ssrContext }) {
  let service = new Service(app.axios, ssrContext)
}
```

###7 - Simplified Injection

If you don't want to repeat your self a lot, you can create an injection helper like this:

```js
import Vue from 'vue'

const mixins = []
const inject = function (bootCb) {
  return async function (ctx) {
    const { app, router, store } = ctx
    let boot
    if (typeof bootCb === 'function') {
      const response = bootCb(ctx)
      boot = response.then ? await response : response
    } else {
      boot = bootCb
    }

    for (const name in boot) {
      const key = `$${name}`
      if (mixins.indexOf(name) === -1) {
        mixins.push(name)
        Vue.mixin({
          beforeCreate () {
            const options = this.$options
            if (options[name]) {
              this[key] = options[name]
            } else if (options.parent) {
              this[key] = options.parent[key]
            }
          }
        })
      }
      app[name] = boot[name]
      store[key] = boot[name]
      router[key] = boot[name]
    }
  }
}

export default inject
```

So, modify the axios boot to use the created helper:

```js
import axios from 'axios'
import { Cookies } from 'quasar'

export default inject(async function ({ ssrContext }) {
  let instance = axios.create({
    baseURL: 'https://api.example.com'
  })

  const cookies = process.env.SERVER
    ? Cookies.parseSSR(ssrContext)
    : Cookies

  instance.interceptors.request.use(function (config) {
    const token = cookies.get('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  }, function (error) {
    return Promise.reject(error)
  })

  return {
    axios: instance
  }
})
```

I hope this article will help you get your cookies under control, when working with Quasar's SSR feature. Let us know how you work with cookies or where you've had problems related to cookies and SSR and solved them. We'd love to hear about that in the comments below.

###8 - About Quasar

Interested in Quasar? Here are some more tips and information:

More info: https://quasar.dev
GitHub: https://github.com/quasarframework/quasar
Newsletter: https://quasar.dev/newsletter
Getting Started: https://quasar.dev/start
Chat Server: https://chat.quasar.dev/
Forum: https://forum.quasar.dev/
Twitter: https://twitter.com/quasarframework
Donate: https://donate.quasar.dev