Keep Vuex state data without vuex-persist - vue.js

weird question but i don't find an answer anywhere..
I return user data from an API call to Vuex. I save my user object into the Vuex state, along with a Token. (User object and Token are created and send back from Server to Vuex at the same time.)
Everything runs perfect and on the initialization of the component i fetch with a getter the user name etc.
But when i refresh i loose the user object from the state. But, i do not loose the Token. Which is weird cause i create them and return them together.
The question is, how can i keep the user in the state until i logout?
I don't need to keep them in localStorage or inside a cookie cause they are sensitive data (user). I just want to get them through a getter from my store. Which is the correct way to do it.
So vuex-persist is not an option..
Below you see my code:
store.js:
state: {
status: '',
token: localStorage.getItem('token'),
user: {}
},
mutations: {
auth_success(state, { token, user }) {
state.status = 'success';
state.token = token;
state.user = user;
},
actions: {
login({ commit }, user) {
return new Promise((resolve, reject) => {
commit('auth_request');
axios({
url: 'http://localhost:8085/login',
data: user,
method: 'POST'
.then((resp) => {
const token = resp.data.token;
const user = resp.data.user;
axios.defaults.headers.common['Authorization'] = token;
commit('auth_success', { token, user });
})
.catch((err) => {
commit('auth_error');
localStorage.removeItem('token');
reject(err);
});
}
},
getters: {
isLoggedIn(state) {
return state.token;
},
getUser(state){
return state.user;
}
User.vue:
<template>
<v-container>
<v-layout row wrap>
Welcome {{this.user.fullName}}
</v-layout>
</v-container>
</template>
<script>
export default {
data: function() {
return {
user: {}
}
},
mounted() {
this.getUser();
},
methods: {
getUser() {
return (this.user = this.$store.getters.getUser);
}
}
}
</script>
So to sum up:
Token stays in Vuex, user data does not. How to keep them in state without local Storage or cookies?
Any help would be greatly appreciated!

Basically, as Sang Đặng mentioned, if you want to have user data in your vuex (without storing it on the user side) you need to fetch them after every refresh. Refreshing the page means that whole Vue application (and your Vuex state) is removed from the memory (user's browser), which causes that you lose your current store data. token is also removed from the memory, but you load it on your store initialisation:
state: {
token: localStorage.getItem('token'),
...
}
Because of this you are seeing token "kept" in store, while other user data not. There are many ways to fetch user data after refresh - like mentioned beforeRouteEnter. Basically if you want to fetch them on the application load, so you can use Vue.created hook for example. You can also use lazy-loading in your getUser method - if there is no user data - fetch them from your API. Here you can read more about authentication patterns in SPA - for example using OAuth.

Related

Vue 3, Vue Router 4 Navigation Guards and Pinia store

I'm trying to create an Vue 3 with app with JWT authentication and meet an issue with guarding the router using "isAuth" variable from Pinia store to check the access. Eventually Vue router and app in whole loads faster than the Store, that's why I'm always getting "unauthorized" value from the store, but in fact user is logged in and his data is in store.
I'll try to describe all the steps that are made to register and login user.
Registration is made to NodeJS backend and JWT token is created.
On the login screen user enters email and password, if info is valid he will be logged in and JWT will be saved to localstorage and decoded through JWTdecode, decoded token data will be saved to the store in user variable, and isAuth variable set to true.
Pinia store has 2 fields in state: user(initially null), and isAuth(initially false).
In the main App component I'm using async onMounted hook to check the token and keep user logged in by calling the API method, which compares JWT.
In the Vue router i have several routes that must be protected from the unauthorized users, that's why I'm trying to create navigation guards for them by checking the user information from the store. Problem is, router is created after the setting user info and is always getting the initial state of the user and isAuth variables.
Code:
Store
import { defineStore } from 'pinia';
export const useLoggedInUserStore = defineStore({
id: 'loggedInUser',
state: () => ({
isAuth: false,
user: null
}),
getters: {
getisAuth(state) {
return state.isAuth;
},
getUser(state) {
return state.user;
}
},
actions: {
setUser(user) {
this.user = user;
},
setAuth(boolean) {
this.isAuth = boolean;
}
}
});
App.vue onMounted
onMounted(async () => {
await checkUser()
.then((data) => {
isLoading.value = true;
if (data) {
setUser(data);
setAuth(true);
} else {
router.push({ name: 'Login' });
}
})
.finally((isLoading.value = false));
});
Router guard sample
router.beforeEach((to, from, next) => {
const store = useLoggedInUserStore();
if (!store.isAuth && to.name !== 'Login') next({ name: 'Login' });
else next();
});
I feel that problem is with this async checking, but can't figure out how to rewrite it to load store before the app initialization.
I hope that somebody meet this problem too and can help.
Thanks in advance!
So I just met this problem and fixed it thanks to this solution
As it says, the router gets instantiated before App.vue is fully mounted so check the token in beforeEach instead, like:
router.beforeEach(async (to, from, next): Promise<void> => {
const user = useUser();
await user.get();
console.log(user) // user is defined
if (to.meta.requiresAuth && !user.isLoggedIn) next({ name: "home" }); // this will work
By the way instead of having an action setAuth you could just use your getter isAuth checking if user is not null, like:
isAuth: (state) => state.user !== null
Also it's not recommended to store a JWT in the local storage as if you're site is exposed to XSS attacks the token can be stolen. You should at least store it in an HttpOnly cookie (meaning it's not accessible from JavaScript), it's super easy to do with Express.

vue/vuex: Can you re-render a page from another page?

With the first login in my app, users get a possibility to leave their address. When this address is stored, the user are pushed to their dashboard. Second login the user go straight to the dashboard.
I have 2 Vuex states that are updated with the response.data. 'Signed' leads to address page, 'Frequent' leads to 'dashboard'.
//PROMPT.VUE
mounted () {
this.getPrompt()
},
computed: {
promptStatus () {
return this.$store.getters.getPrompt
}
},
methods: {
async getPrompt() {
try{
await //GET axios etc
// push prompt status in Store
let value = response.data
this.$store.commit('setPrompt', value)
if (this.promptStatus === 'signed') {
this.$router.push({path: '/adres'})
}
if (this.promptStatus === 'frequent') {
this.$router.push({path: '/dashboard'})
}
When user leaves the address I reset the vuex.state from 'signed' to 'frequent'.
//ADRES.VUE
//store address
let value = 'frequent'
this.$store.commit('setPrompt', value)
this.$router.push({name: 'Prompt'})
The Vuex.store is refreshed. But the Prompt.vue wil not re-render with the new vuex.status. Many articles are written. Can 't find my solution. Maybe I organize my pages the wrong way.
In views, it is not recommended to mutate data (call commit) outside vuex. Actions are created for these purposes (called from the component using dispatch). In your case, you need to call action "getPrompt" from the store, but process routing in the authorization component. This is more about best practice
To solve your problem, you need to make a loader when switching to dashboard. Until the data is received, you do not transfer the user to the dashboard page
Example
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "DashboardLayout",
components: { ..., ... },
data: () => ({
isLoad: false
}),
async created() {
this.isLoad = false;
try {
await this.$store.dispatch('getData');
this.isLoad = true;
} catch (error) {
console.log(error)
}
}
});
</script>
Data is received and stored in the store in the "getData" action.
The referral to the dashboard page takes place after authorization. If authorization is invalid, the router.beforeEach handler (navigation guards) in your router/index.js should redirect back to the login page.
Learn more about layout in vuejs
Learn more about navigation guards

Auth not accessible in vuex-module after page reload or direct access

I have an authentication on my nuxt web-app, using the nuxt/auth module. I also use modular vuex stores to handle different states. After I login, everything is fine and I can navigate through the app normally. But when I try to reload the page or access it directly through a URL, the user is not accessible, thus, the whole web-app becomes unusable. I try to access the user object with this.context.rootState.auth.user, which is null after page-reload or direct access. Strangely enough, this only happens in production.
I already tried to add an if-guard, but sadly the getter is not reactive. Probably because it´s a nested object. This is my current getter:
get someGetter() {
if (!this.context.rootState.auth.user) {
return []
}
const userId = this.context.rootState.auth.user.id as string
const arr = []
for (const item of this.items) {
// Using userId to add something to arr
}
return arr
}
Is there a way to force nuxt to finish the authentication before initialising the vuex-modules, or to make this getter reactive, so it will trigger again, when the user object is accessible?
This is what my auth-config looks like in nuxt.config.ts:
auth: {
strategies: {
local: {
_scheme: '#/auth/local-scheme',
endpoints: {
login: {
url: '/api/authenticate',
method: 'post',
propertyName: false
},
logout: { url: '/api/logout', method: 'post' },
user: { url: '/api/users/profile', propertyName: false }
}
},
// This dummy setting is required so we can extend the default local scheme
dummy: {
_scheme: 'local'
}
},
redirect: {
logout: '/login'
}
}
EDIT
I resolved this by following Raihan Kabir´s answer. Using vuex-persistedstate in an auth-plugin, which is triggered every time the server renders the page. The plugin saves the userId in a cookie, so the store can use it as a fallback, if the auth-module isn´t ready.
The thing is, the vuex clears data on reload/refresh to keep credentials secure. That's what vuex is. If you want to store data for long time without being interrupted after reloading, you should use localstorage for that. But localstorage is not recommended for storing credentials.
If you need only user_id to keep in the vuex, use Cookie instead. And try something like this in your store's index.js file -
export const actions = {
// This one runs on the beginning of reload/refresh
nuxtServerInit ({ commit }, { req }) {
if (req.headers.cookie) {
const parsed = cookieparser.parse(req.headers.cookie)
try {
// get user id that you would set on auth as Cookie
user_id = parsed.uid
} catch (err) {
// error here...
}
}
// perform login and store info on vuex store
commit('authUserOnReload', user_id)
},
}
// Define Mutations
export const mutations = {
authUserOnReload (state, user_id) {
// perform login here and store user
}
}

How to deduplicate data fetches in children routes

Imagine the following routes. I'm using Vue and vue-router syntax right now, but I figure the question applies to other SPA frameworks as well.
{
path: 'user/:id', component: require('User.vue'),
children: [
{ path: 'edit', component: require('UserEdit.vue'), }
]
}
In User.vue, the user object is fetched using the route id parameter upon component creation:
data() {
return { user: null }
},
created() {
this.user = fetchUser(this.$route.params.id)
}
In UserEdit.vue, a user is also fetched, and in 85% of the cases this will be the user that was also fetched in User.vue:
data() {
return { user: null }
},
created() {
this.user = fetchUser(this.$route.params.id)
}
Question: if we would navigate from User.vue to UserEdit.vue, it is apparent that (most probably) the same user object will be fetched again. How can this kind of code duplication be avoided? How should I pass the previously fetched data down to a child route?
I guess I should somewhere check if the route parameters remain equal, because if they aren't we're editing another user and the User data should be fetched anyway...
Time for a state management store (like vuex)? If so, when the app navigates away from user pages, should the user store be cleared, or do you keep the last fetched user always in memory?
I'm having a hard time to come up with something DRY.
Looking forward to your advice and some hands-on code examples.
Use vuex for state management. For example, setting something like lastUser and userData which could be accessed from any component. fetchUser would then be an action in the store:
Store
state: {
lastUser: '',
userData: null
},
actions: {
fetchUser({ state }, user) {
if (state.userData && user == state.lastUser) {
return state.userData;
} else {
// Api call, set userData and lastUser, return userData
}
}
}
User
async created() {
this.user = await this.$store.dispatch('fetchUser', this.$route.params.id);
}
UserEdit
async created() {
this.user = await this.$store.dispatch('fetchUser', this.$route.params.id);
}

vuex getter does not update on another component depending on timing

My app uses
axios to fetch user information from a backend server
vuex to store users
vue-router to navigate on each user's page
In App.vue, the fetch is dispatched
export default {
name: 'app',
components: {
nav00
},
beforeCreate() {
this.$store.dispatch('fetchUsers')
}
}
In store.js, the users is an object with pk (primary key) to user information.
export default new Vuex.Store({
state: {
users: {},
},
getters: {
userCount: state => {
return Object.keys(state.users).length
}
},
mutations: {
SET_USERS(state, users) {
// users should be backend response
console.log(users.length)
users.forEach(u => state.users[u.pk] = u)
actions: {
fetchUsers({commit}) {
Backend.getUsers()
.then(response => {
commit('SET_USERS', response.data)
})
.catch(error => {
console.log("Cannot fetch users: ", error.response)
})
})
Here Backend.getUsers() is an axios call.
In another component which map to /about in the vue-router, it simply displays userCount via the getter.
Now the behavior of the app depends on timing. If I visit / first and wait 2-3 seconds, then go to /about, the userCount is displayed correctly. However, if I visit /about directly, or visit / first and quickly navigate to /about, the userCount is 0. But in the console, it still shows the correct user count (from the log in SET_USERS).
What did I miss here? Shouldn't the store getter see the update in users and render the HTML display again?
Since it's an object Vue can't detect the changes of the properties and it's even less reactive when it comes to computed properties.
Copied from https://vuex.vuejs.org/guide/mutations.html:
When adding new properties to an Object, you should either:
Use Vue.set(obj, 'newProp', 123), or
Replace that Object with a fresh one. For example, using the object spread syntax we can write it like this:
state.obj = { ...state.obj, newProp: 123 }