Getting Quasar’s SSR cookie plugin working with other libraries and services.
Table Of Contents
- 1 Introduction
- 2 The Problem
- 3 The Solution
- 4 Vuex's Stores
- 5 Navigation Guards
- 6 Other Services
- 7 Simplified Injection
- 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:
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
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
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
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
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
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
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.
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
.
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:
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:
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:
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
Top comments (16)
I changed the
axios
boot file like below by addingVue.mixin
instead ofVue.prototype
. But now when I try to accessthis.$axios.post()
in.vue
component its throwingTypeError: Cannot read property 'post' of undefined
. So the global mixin is not passing into components. I simply copied the mixin logic from here.Any insights on what could be the issue here?
this would be:
Thank you.
Great article! Can you please show your final implementation on Github repo?
really sorry, i missed your comment.:
gitlab.com/TobyMosque/quasar-couch...
I think this link is not for this article as this code is missing there. Could you please share the correct link if possible?
ops, i didn't created a repo to this article, mostly because this is a simplified version of what i do.
Normally, I didn't access the cookies directly, i use
vuex-persistedstate
as a middleware.but u can access the Cookies directly, there is nothing wrong with this approach
github.com/TobyMosque/qpanc/blob/m...
And here the axios part:
github.com/TobyMosque/qpanc/blob/m...
I'll try to create a repo to this article at the weekend.
Ok. Thank you for the support
This guide has been invaluable, thank you. Just one thing I'm trying to puzzle out - what is the purpose of the
beforeCreate()
code here? Is there a reason to prefer this over injecting the Axios instance in the Vue prototype?Vue.prototype
is shared between allVue
instances, what will result in a singleaxios
instance to allVue
instances (a singleton).But in that particular case, a singleton can mess with our autentication logic, since that is being handled into the
axios
interceptor and this reads the token from the Cookies plugin, what isn't a singleton, what can lead to a data leak.Thank you man!
Thank you for the excellent article! It would be nice to have this added to the Quasar documentation (if it is not there already), as it is rather square one if you want to implement SSR. I'd have one small remark, under 5 Navigation guards, in ./src/router/index.js, I had to switch context.router.beforeEnter to context.router.beforeEach. As far as I know, there is no global guard beforeEnter now in Vue Router, I'm not sure for older versions.
probably a small typo, thx for report.
Also a great Article and perfect understandable written.
Thank you for sharing your know how.
how can I get app context for call my other service?
I didn't understood u question