Managing Vuex state using with NuxtJs on 'universal' mode - vuex

I get used to using localStorage with Vuex but as you know on SSR we can not use localeStorage. So I searched a solution to keep Vuex state on page refresh and run into vuex-persistedstate using with js-cookie many times.
My question is that do I need really vuex-persistedstate? I mean that can I just use js-cookie just like the following:
import Cookie from 'js-cookie'
const state = {
firstName: Cookie.get('firstName)
}
const mutations = {
setFirstName(state, name) {
state.firstName = name;
Cookie.set('firstName', name)
}
etc.

Following your comment question. Your example in the question is ok at least for setting the cookie. I set mine in an action first and then commit the mutation to set the state only. As for getting your cookies, you might need a bit more than just setting the state. As I said i use 'cookie' (see here) for getting my cookies and I get them through nuxtServerInit, like this:
nuxtServerInit({dispatch}, context) {
return Promise.all([
dispatch('get_cookies', context),
//other actions
]);
},
and the action itself as follows:
get_cookies (vuexContext, context) {
return new Promise((resolve, reject) => {
const cookieConst = cookie.parse(context.req.headers.cookie || '')
if (cookieConst.hasOwnProperty('YourCookieName')) {
//commit mutations here
}
else {
resolve(false)
}
})
},
Obviously you need to import:
import cookies from 'js-cookie'
import cookie from 'cookie'
You can put a fair bit of stuff into cookies, I set user details and shopping cart. I also store an access token in my cookies and validate it in my backend so I can persist an auth state.

Related

How can I access the react-admin store in the dataprovider? [React Admin 4.x]

I'm trying to make use of a user-set variable in the react admin store when making API calls.
Specifically, I am storing a workspace ID in the store, which the user can set through a switcher. I want to be able to access this ID when making API calls, so that I can send over the workspace ID as a url parameter in the API request.
One soluion is to try to get the data directly from localstorage, but that seems hacky. Is there a btter way?
I'm using the latest version of react admin
In recent versions of React-admin, pass parameters like this:
"React-admin v4 introduced the meta option in dataProvider queries, letting you add extra parameters to any API call.
Starting with react-admin v4.2, you can now specify a custom meta in <List>, <Show>, <Edit> and <Create> components,
via the queryOptions and mutationOptions props."
https://marmelab.com/blog/2022/09/05/react-admin-septempter-2022-updates.html#set-query-code-classlanguage-textmetacode-in-page-components
import { Edit, SimpleForm } from 'react-admin'
const PostEdit = () => (
<Edit mutationOptions={{ meta: { foo: 'bar' } }}>
<SimpleForm>...</SimpleForm>
</Edit>
)
#MaxAlex approach works well, but I went with a localStorage route, by setting a header in the fetchHTTP client defined with the dataprovider. This way I didn't have to modify each and every route.
const httpClient = (url: any, options: any) => {
if (!options) {
options = {};
}
if (!options.headers) {
options.headers = new Headers({ Accept: 'application/json' });
}
const token = inMemoryJWT.getToken();
const organization = localStorage.getItem('RaStore.organization');
if (token) {
options.headers.set('Authorization', `Bearer ${token}`);
} else {
inMemoryJWT.getRefreshedToken().then((gotFreshToken) => {
if (gotFreshToken) {
options.headers.set(
'Authorization',
`Bearer ${inMemoryJWT.getToken()}`,
);
}
});
}
if (organization) {
options.headers.set('Organization-ID', organization);
}
return fetchUtils.fetchJson(url, options);
};
I also looked into the react admin internals, and the store provider is one level below the dataprovider. This means there isn't an easy way to access the store without refactoring the entire Admin provider stack.

Insert localstorage with vuex

My script I'm using axios and vuex but it was necessary to make a change from formData to Json in the script and with that it's returning from the POST/loginB2B 200 api, but it doesn't insert in the localstorage so it doesn't direct to the dashboard page.
**Auth.js**
import axios from "axios";
const state = {
user: null,
};
const getters = {
isAuthenticated: (state) => !!state.user,
StateUser: (state) => state.user,
};
async LogIn({commit}, user) {
await axios.post("loginB2B", user);
await commit("setUser", user.get("email"));
},
async LogOut({ commit }) {
let user = null;
commit("logout", user);
},
};
**Login.vue**
methods: {
...mapActions(["LogIn"]),
async submit() {
/*const User = new FormData();
User.append("email", this.form.username)
User.append("password", this.form.password)*/
try {
await this.LogIn({
"email": this.form.username,
"password": this.form.password
})
this.$router.push("/dashboard")
this.showError = false
} catch (error) {
this.showError = true
}
},
},
app.vue
name: "App",
created() {
const currentPath = this.$router.history.current.path;
if (window.localStorage.getItem("authenticated") === "false") {
this.$router.push("/login");
}
if (currentPath === "/") {
this.$router.push("/dashboard");
}
},
};
The api /loginB2B returns 200 but it doesn't create the storage to redirect to the dashboard.
I use this example, but I need to pass json instead of formData:
https://www.smashingmagazine.com/2020/10/authentication-in-vue-js/
There are a couple of problems here:
You do a window.localStorage.getItem call, but you never do a window.localStorage.setItem call anywhere that we can see, so that item is probably always empty. There also does not seem to be a good reason to use localStorage here, because you can just access your vuex store. I noticed in the link you provided that they use the vuex-persistedstate package. This does store stuff in localStorage by default under the vuex key, but you should not manually query that.
You are using the created lifecycle hook in App.vue, which usually is the main component that is mounted when you start the application. This also means that the code in this lifecycle hook is executed before you log in, or really do anything in the application. Instead use Route Navigation Guards from vue-router (https://router.vuejs.org/guide/advanced/navigation-guards.html).
Unrelated, but you are not checking the response from your axios post call, which means you are relying on this call always returning a status code that is not between 200 and 299, and that nothing and no-one will ever change the range of status codes that result in an error and which codes result in a response. It's not uncommon to widen the range of "successful" status codes and perform their own global code based on that. It's also not uncommon for these kind of endpoints to return a 200 OK status code with a response body that indicates that no login took place, to make it easier on the frontend to display something useful to the user. That may result in people logging in with invalid credentials.
Unrelated, but vuex mutations are always synchronous. You never should await them.
There's no easy way to solve your problem, so I would suggest making it robust from the get-go.
To properly solve your issue I would suggest using a global navigation guard in router.js, mark with the meta key which routes require authentication and which do not, and let the global navigation guard decide if it lets you load a new route or not. It looks like the article you linked goes a similar route. For completeness sake I will post it here as well for anyone visiting.
First of all, modify your router file under router/index.js to contain meta information about the routes you include. Load the store by importing it from the file where you define your store. We will then use the Global Navigation Guard beforeEach to check if the user may continue to that route.
We define the requiresAuth meta key for each route to check if we need to redirect someone if they are not logged in.
router/index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import store from '../store';
Vue.use(VueRouter);
const routes = [
{
path: '/',
name: 'Dashboard',
component: Dashboard,
meta: {
requiresAuth: true
}
},
{
path: '/login',
name: 'Login',
component: Login,
meta: {
requiresAuth: false
}
}
];
// Create a router with the routes we just defined
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
// This navigation guard is called everytime you go to a new route,
// including the first route you try to load
router.beforeEach((to, from, next) => {
// to is the route object that we want to go to
const requiresAuthentication = to.meta.requiresAuth;
// Figure out if we are logged in
const userIsLoggedIn = store.getters['isAuthenticated']; // (maybe auth/isAuthenticated if you are using modules)
if (
(!requiresAuthentication) ||
(requiresAuthentication && userIsLoggedIn)
) {
// We meet the requirements to go to our intended destination, so we call
// the function next without any arguments to go where we intended to go
next();
// Then we return so we do not run any other code
return;
}
// Oh dear, we did try to access a route while we did not have the required
// permissions. Let's redirect the user to the login page by calling next
// with an object like you would do with `this.$router.push(..)`.
next({ name: 'Login' });
});
export default router;
Now you can remove the created hook from App.vue. Now when you manually change the url in the address bar, or use this.$router.push(..) or this.$router.replace(..) it will check this function, and redirect you to the login page if you are not allowed to access it.

Middleware executing before Vuex Store restore from localstorage

In nuxtjs project, I created an auth middleware to protect page.
and using vuex-persistedstate (also tried vuex-persist and nuxt-vuex-persist) to persist vuex store.
Everything is working fine when navigating from page to page, but when i refresh page or directly land to protected route, it redirect me to login page.
localStorage plugin
import createPersistedState from 'vuex-persistedstate'
export default ({ store }) => {
createPersistedState({
key: 'store-key'
})(store)
}
auth middleware
export default function ({ req, store, redirect, route }) {
const userIsLoggedIn = !!store.state.auth.user
if (!userIsLoggedIn) {
return redirect(`/auth/login?redirect=${route.fullPath}`)
}
return Promise.resolve()
}
I solved this problem by using this plugin vuex-persistedstate instead of the vuex-persist plugin. It seems there's some bug (or probably design architecture) in vuex-persist that's causing it.
With the Current approach, we will always fail.
Actual Problem is Vuex Store can never be sync with server side Vuex store.
The fact is we only need data string to be sync with client and server (token).
We can achieve this synchronization with Cookies. because cookies automatically pass to every request from browser. So we don't need to set to any request. Either you just hit the URL from browser address bar or through navigation.
I recommend using module 'cookie-universal-nuxt' for set and remove of cookies.
For Setting cookie after login
this.$cookies.set('token', 'Bearer '+response.tokens.access_token, { path: '/', maxAge: 60 * 60 * 12 })
For Removing cookie on logout
this.$cookies.remove('token')
Please go through the docs for better understanding.
Also I'm using #nuxt/http module for api request.
Now nuxt has a function called nuxtServerInit() in vuex store index file. You should use it to retrieve the token from request and set to http module headers.
async nuxtServerInit ({dispatch, commit}, {app, $http, req}) {
return new Promise((resolve, reject) => {
let token = app.$cookies.get('token')
if(!!token) {
$http.setToken(token, 'Bearer')
}
return resolve(true)
})
},
Below is my nuxt page level middleware
export default function ({app, req, store, redirect, route, context }) {
if(process.server) {
let token = app.$cookies.get('token')
if(!token) {
return redirect({path: '/auth/login', query: {redirect: route.fullPath, message: 'Token Not Provided'}})
} else if(!isTokenValid(token.slice(7))) { // slice(7) used to trim Bearer(space)
return redirect({path: '/auth/login', query: {redirect: route.fullPath, message: 'Token Expired'}})
}
return Promise.resolve()
}
else {
const userIsLoggedIn = !!store.state.auth.user
if (!userIsLoggedIn) {
return redirect({path: '/auth/login', query: {redirect: route.fullPath}})
// return redirect(`/auth/login?redirect=${route.fullPath}`)
} else if (!isTokenValid(store.state.auth.tokens.access_token)) {
return redirect({path: '/auth/login', query: {redirect: route.fullPath, message: 'Token Expired'}})
// return redirect(`/auth/login?redirect=${route.fullPath}&message=Token Expired`)
} else if (isTokenValid(store.state.auth.tokens.refresh_token)) {
return redirect(`/auth/refresh`)
} else if (store.state.auth.user.role !== 'admin')
return redirect(`/403?message=Not having sufficient permission`)
return Promise.resolve()
}
}
I have write different condition for with different source of token, as in code. On Server Process i'm getting token from cookies and on client getting token store. (Here we can also get from cookies)
After this you may get Some hydration issue because of store data binding in layout. To overcome this issue use <no-ssr></no-ssr> wrapping for such type of template code.

Nuxt js <nuxt-link /> authentication

I have a route in nuxt that has to be accessible only by logged in users: /dashboard/secret.
In /dashboard page I have a link like this:
<nuxt-link to="/dashboard/secret">Link to "secret" page</nuxt-link>
When clicked, nuxt will fetch that page from
myapp.com/_nuxt/pages_dashboard_secret.js
How can I add authentication for that nuxt route to stop people from manually going to that url and reading the contents?
Yes the actual secret data will be taken from external api which will validate user token, but still it seems wrong that people can see even the html of this page
if you just want to protect a js file, it would be wrong to do it like this. But if you mean you just want to protect a route from being accessed manually by the users, you must try Nuxt Middlewares and write a middleware for authentication and user fetching.
The middleware structure can be as simple as this:
export default function ({ store, redirect }) {
// If the user is not authenticated
if (!store.state.authenticated) {
return redirect('/login')
}
}
and you can simply use it like this in your root (or secretPage) layout:
<template>
<h1>Secret page</h1>
</template>
<script>
export default {
middleware: 'authenticated'
}
</script>
You can use nuxt/auth package, that is the case for your work and can be used as a plugin and module, you can check has it for the be accessible page or not, it runs automatically and has semantic structure.
You cannot keep your secret on client side (in your JS code) everyone using your application can get it from his browser. You need to keep secret keys on server side and make some validation endpoint to provide if user is valid or not or just keep his session after login.
you can use middleware in nuxt framework. Also, route has a information about url and request. You can make a logic by using them.
https://nuxtjs.org/docs/directory-structure/middleware/
middleware/auth.js
export default async function ({store, from, route, req}) {
if (process.client) {
if (route.name === 'dashboard-room-id' && from.name === route.name)
return
else await store.dispatch('checkSession', route)
}
}
save the token in the store on nuxtServerInit or whenever you get it.
on /dashboard/secret page check in the fetch method if there is a token set.
if token is set, fetch your data otherwise redirect the use somewhere else
https://nuxtjs.org/examples/auth-routes/#redirect-user-if-not-connected
For such a guard of pages, the middleware is the sure way to do it.
Create a middleware file in the middleware directory
Add your middleware logic as described here https://nuxtjs.org/api/pages-middleware/
Then add the middleware option in your page component
as it is mentioned that the routing should be done on the server, in case you just want to handle it if I have this
store/index.js action
async nuxtServerInit({ dispatch, commit }, { req }) {
try {
if (process.server && process.static) { return }
if (!req.headers.cookie) {
console.log('return ')
return
}
const parsed = cookieparser.parse(req.headers.cookie)
const accessTokenCookie = parsed.__session
if (!accessTokenCookie) { return }
const decoded = JWTDecode(accessTokenCookie)
if (userData.exists) {
commit('setState', { name: 'user',
value: {
uid: decoded.user_id,
email: decoded.email,
...userData.data()
} })
}
} catch (e) {
console.log(e)
}
},
//Login firebase
async fireLogin({ dispatch }, { singInWith, account }) {
const resp = await this.$firebase.auth()signInWithEmailAndPassword(account.email, account.password)
const token = await resp.user.getIdToken()
Cookie.set('__session', token)
return { email: resp.user.email, uid: resp.user.uid }
}
Middleware/auth.js
export default function({ store, route, redirect }) {
const user = store.state.user
const blockedRoute = /\/admin\/*/g
const homeRoute = '/'
if (!user && route.path.match(blockedRoute)) {
redirect('/')
}
/*if (user && route.path === homeRoute) {
redirect('/admin')
}*/
}
nuxt.config
router: {
middleware: [
'authenticated'
]
},
you can set the middleware for current page
middle ware
export default context => {
//set Condition and logic
};
route page :
middleware: 'name of middle ware'
i can suggest three solutions:
1.Get pathname in your js codes and then check the url that client using to access your page , for example if pathname is
/dashboard/secret and user is logged in then show the page
for checking pathname u can use these cods:
$nuxt.$route.path
//or good old pure js ;)
window.location.pathname
2.check if user truly logged in (backend & frontend)
for that u can use nuxt-auth and sync it to your backend as well.
for example if you using laravel , u can use laravel passport ,
in that case when the request sended to the backend route, you can check if user is logged in to the backend as well.
Ps:This way is more secure and of course in every backend language this process can be different, but surely all of them will have the same capability.
3.using .htaccess :
Do not allow the user to view the file directly from the server path
Read more

Why is my Vuex getter returning TRUE on the client side and FALSE on the server side?

I'm using NUXT middleware to check if a user is logged in or not, and protect certain routes accordingly. The problem is, when a logged-in user refreshes the page on one of the protected routes, the session is lost.
I have a getter in my Vuex store state (using NUXT):
getters: {
isLoggedIn (state) {
return !isEmpty(state.auth.email) && !isEmpty(state.auth.token)
}
}
I'm accessing this getter in middleware to redirect unauthenticated users to a login page:
let isLoggedIn = context.store.getters.isLoggedIn
if (!isLoggedIn && protectedRoutes.includes(context.route.name)) {
let language = context.store.language ? context.store.language : 'en'
context.redirect(`/${language}/login`)
}
But it's not working. When I console.log() the value of this getter, I get TRUE on the client side and FALSE on the server side. How can I keep them both in sync with Vue/Vuex?
Furthermore, whenever I console.log() the context object on the server side, it appears to be in its initial state. There must be something fundamentally wrong with my approach.
When user refresh a page all vuex state is lost and start from new. You need to initialize user somewhere like nuxtServerInit
actions: {
nuxtServerInit ({ commit }, { req }) {
if (req.session.user) {
commit('user', req.session.user)
}
}
}