dmytroshved's avatar

Vue: Pinia Storage in Router guard

Hi

I need to restrict access for authenticated users to the /login and /register routes

My Pinia Storage user.js contains a computed property isAuthorized. However when I try to use this variable in my router/index.js I see it always has a false instead of true.

Also F12 -> Vue -> Pinia shows isAuthorized = true, but in code it shows as false

I also have a plugin set-user that takes the current authenticated user from backend and saves him to the Pinia Store user.js

store/user.js

import { computed, ref } from 'vue'
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', () => {
  const user = ref(null)

  const isAuthorized = computed(() => !!user.value)

  function updateUser(userData) {
    userData ? user.value = userData : user.value = null
  }

  return {
    user,
    isAuthorized,
    updateUser
  }
})

router/index.js

plugins/set-user.js

import { getCurrentUser } from '@/api/user.js'
import { useUserStore } from '@/store/user.js'

export default async function setUser(pinia) {
  const { updateUser } = useUserStore(pinia)

  const userData = await getCurrentUser()

  userData ? updateUser(userData) : updateUser(null)
}

main.js

import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';
import './style.css';
import setUser from '@/plugins/set-user.js'

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)
app.use(router)

setUser(pinia)

app.mount('#app')

Would be grateful for your help

Best regards

0 likes
6 replies
Shivamyadav's avatar
Level 20

Try this you are mounting the router earlier.

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import setUser from '@/plugins/set-user.js'

async function bootstrap() {
  const app = createApp(App)
  const pinia = createPinia()

  app.use(pinia)

  //  Wait until user is loaded BEFORE router runs guards
  await setUser(pinia)

  app.use(router)
  app.mount('#app')
}
1 like
dmytroshved's avatar

It works only if I have authenticated user, if his session burns out I see only blank page and message that GET /user returns 401 Atuh error from backend, which means setUser failed

I added try..catch so application can mount even if user didn't found

import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';
import './style.css';
import setUser from '@/plugins/set-user.js'

async function bootstrap() {
  const app = createApp(App)
  const pinia = createPinia()

  app.use(pinia)

  try{
    //  Wait until user is loaded BEFORE router runs guards
    await setUser(pinia)
  } catch (error){
    console.log('Unauthorized')
  }

  app.use(router)
  app.mount('#app')
}

bootstrap()

What you think?

shahriar_shaon's avatar

The issue happens because the router is creating a new Pinia store instance when you call useUserStore() inside the router guard. This latest instance does not contain the authenticated user, so isAuthorized always shows false.

router/index.js

import { useUserStore } from '@/store/user'
import pinia from '@/store'

router.beforeEach((to, from, next) => {
  const userStore = useUserStore(pinia)

  if ((to.name === 'login' || to.name === 'register') && userStore.isAuthorized) {
    return next('/')
  }

  next()
})
1 like
dmytroshved's avatar

Here is the summary of changes after all refactors and researches

Appreciate for help: @shivamyadav, @shahriar_shaon and StackOverflow


Key points to pay attention to

  • there's no reason to keep setUser and getCurrentUser separate from the store, a store is not just a global state but can contain related business logic.

  • there's supposed to be a global auth guard instead of per route beforeEnter, this is where logic takes place:

router.beforeEach(async (to, from) => {
  if (from === START_LOCATION) {
    const userStore = useUserStore();
    await userStore.getUser();
  }
})
  • any initialization logic can be run like that. It's convenient to declaratively mark routes that need auth with meta: { auth: true } with this guard and run auth logic only if to.meta.auth === true.

  • it needs to be taken into account that initial navigation starts before an application is mounted. In order to avoid possible race conditions, it can be:

await router.isReady()
app.mount('#app')

Code

main.js

import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';

import './style.css';

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)
app.use(router)

await router.isReady()
app.mount('#app')

router/index.js

store/user.js

Usage

For example here is how my LoginView.vue script looks like

LoginView.vue

1 like
Shivamyadav's avatar

Yeah, I have also struggled with this while learning. So, I remembered a little bit. I'am happy that it helps you.

As for now I have shifted towards the inertia so I just forgotten some amazing tips which I had learnt.

1 like

Please or to participate in this conversation.