A cyclic dependency between 'router' and 'store' in Vuex app - vue.js

I have a vue.js app with a router that prevents the pages from been open without authorization using the following code:
import Router from 'vue-router';
import store from '../store/index';
function guardAuth(to, from, next) {
if (store.state.authorizationToken) {
next();
} else {
next({
name: 'login',
query: { redirect: to.fullPath },
});
}
}
export default new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'toroot',
redirect: 'login',
},
{
path: '/overview',
component: Overview,
beforeEnter: guardAuth,
},
....
and a store mutation that is called when an API call fails:
import axios from 'axios';
import Cookies from 'js-cookie';
import router from '../router/index';
export default new Vuex.Store({
state: {
mutations: {
handleApiFail(state, err) {
if (err && !axios.isCancel(err) && state.authorizationToken) {
// Block subsequent logout calls.
state.authorizationToken = null;
// Clear the token cookie just in case.
Cookies.set('authorizationToken', null);
// Stop the current and subsequent requests.
state.cancellationSource.cancel('Authorization token has expired.');
router.push({ name: 'login', query: { expired: '1', redirect: window.location.pathname } });
}
},
as you can see from the code above 'router' imports 'store' and 'store' imports 'router' and as far as I see this causes 'store' to be undefined inside 'guardAuth()'. Obviously, I can get rid of this cyclic dependency by moving 'handleApiFail' to a separate '.js' file, but I am not sure that it is a good idea. Is there a better solution or some common approach for haling this sutiation? Should 'handleApiFail' be a mutation or a simple function? Can a mutation use 'router'? Do I really need to get rid of the cyclic dependency (for example, in C++ I does not)?

It be better handleapi fail in separate function than mutation. and if you want to check it before entering route. you could use beforeEnter() on your route.
check this docs about beforeEnter or another route properties

Store mutation methods should not perform any logic at all. Stores are only used to hold your global application state, they should not perform any logic like authorizing the user or navigating through your application. What you'll want to do is move the logic out of the store and into the component that does the authorization check. From there just do something like $store.commit('unauthorized') and $store.commit('authorized', user). Should look like this:
sendAuthRequest.then(
(success) => {
$store.commit('authorized', <userVariable>);
$router.push(...);
}, (failure) => {
$store.commit('unauthorized');
$router.push(...);
}
);

Related

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.

Fetching before navigation with multiple components using vue-router

Trying to figure out what is the best way to fetch some data before navigating to some routes that I have.
My routes are:
let routes = [
{
path: '/',
component: require('./views/Home.vue')
},
{
path: '/messages',
component: require('./views/Messages.vue'),
},
{
path: '/posts',
component: require('./views/Post.vue'),
},
{
path: '/login',
component: require('./views/Login.vue')
},
{
path: '/dashboard',
component: require('./views/Dashboard.vue'),
}
];
For /messages, /posts and /dashboard I want to fetch some data and while doing that show a spinner.
Vue-router docs suggest to use beforeRouteEnter, Fetching Before Navigation
beforeRouteEnter (to, from, next) {
getPost(to.params.id, (err, post) => {
next(vm => vm.setData(err, post))
})
}
But my question is, must I implement this fetch logic in all my three view components?
Since it's the same data being fetched I don't want to repeat the logic in every component.
The beforeRouteEnter isn't called in my root component where I have this <router-view></router-view> otherwise I feel that it would be the best place to have this logic.
I'm really new to vue and this one is a really hard nut to crack for me.
Are you aware of vuex? Since you need to share the same data on three different pages, which sounds like a job for vuex store. Basically you can import the store into your router, and set data in the store instead of on the component, and then you can conditionally fetch the data by checking the status of the data in the store and if the data already exists, don't fetch. Something like the following:
import store from './store'
beforeRouteEnter (to, from, next) {
if (store.state.post === null) {
getPost(to.params.id, (err, post) => {
store.dispatch('setPost', post)
next()
})
}
}
Assume your store has a state called post and action called setPost.
const store = new Vuex.Store({
state: { post: null },
actions: {
setPost ({ commit }, post) {
// set the post state here
}
}
})

Unable to understand how vue-router query params work

I am building an app with the following route settings.
export default new Router({
routes: [
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/home',
name: 'Dashboard',
component: Dashboard,
beforeEnter(to, from, next) {
if (!store.getters.isLoggedIn) {
next('/login');
} else {
next();
}
}
},
{
path: '/',
redirect: '/home'
}
]
});
I also set query parameters based on some select boxes I have by using $router.push say as follows.
this.$router.replace({
query: {
scope: this.$store.getters.filterScope
}
});
Problems I have
Reloading the page removes the query params from the URL.
Updating one query parameter removes the other one I had. Example - updating scope using the above method removes the dateRange parameter I already had.
I am using Vuex too so is there any way I can manage these query params using the store?
If you store your parameters in vuex and you don't use vuex-persistedstate, when you refresh the page state will be removed.
So, my suggestion for you:
your-awesome-site.com/dashboard?first=hello&second=hi
In your Dashboard component, you can do like this
mounted () {
console.log(this.$route.params)
// Update vuex state filterScope
}
So you will retrieve your parameters from url and save it in vuex.
Also you can solve your second issue using this.$route.params

Vue.js with Vue-Router AJAX call before application starts

I'm creating simple application with allows to login, restore password and some other actions for logged users.
I am also using Vue-Router. I want to create 3 different types of routes:
allow only for logged users (example: change password)
allow only for unlogged users (example: restore password)
allow for everyone (example: homepage)
I have two functions with are calling before all routes
router.beforeEach(Authentication.OnlyLoggedAllowed)
router.beforeEach(Authentication.OnlyNotLoggedAllowed)
I also want to login user if it's possible (AJAX call to API). To use it I have tried to add code BEFORE route.beforeEach(...)
import Vue from "vue"
import VueRouter from "vue-router"
import VueResource from "vue-resource"
...
Authentication.TryLogin.bind(Vue)();
...
router.beforeEach(Authentication.OnlyLoggedAllowed)
router.beforeEach(Authentication.OnlyNotLoggedAllowed)
new Vue({
router
}).$mount("div#application")
Is it stupid to add function TryLogin() to router.beforeEach(..)? AJAX call after every page change is not really smart.
But I have problem because this.$http in my TryLogin() function returns undefined.
Or maybe I should raw AJAX call if there's no other way.
After all I will make this call synchronous.
There is no shame in using jQuery, for example, for ajax calls.
If you have a global store (using vuex here) you can set a boolean to true after the first TryLogin, so you don't call it more than once after further route changes. Something like that:
import Vue from 'vue';
import Router from 'vue-router';
import store from './store';
Vue.use(Router);
const router = new Router({
mode: 'history',
routes: [
{
path: '/',
component: Home,
},
{
path: '/profile',
component: Profile,
meta: {loggedIn: true}
},
{
path: '/signup',
component: Signup,
meta: {loggedOut: true},
}
],
});
router.beforeEach(async (to, from, next) => {
if (!store.state.userLoaded) {
await $.get('/api/account').then(
// Gets the user (or null) from the server, and
// set userLoaded to true in any case
user => store.commit('updateUser', user),
err => console.error(err)
}
}
// Check if the user needs to be logged in
if (to.meta.loggedIn && !store.state.user) {
return next ({path: '/login', query: {redirectTo: to.fullPath}});
// Check if the user needs to be logged out
} else if (to.meta.loggedOut && store.state.user) {
return next ({path: '/'});
}
// We can proceeed
next();
}
export default router;
The store.commit('updateUser', user) will set userLoaded to true regardless of whether the user is actually logged in.
Here is an example of the vuex store used:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
user: null,
userLoaded: false
},
mutations: {
updateUser: (state, user) => {
state.user = user;
state.userLoaded = true;
}
}
});
And the main vue file:
import router from './router';
import store from './store';
new Vue({
router,
store
}).$mount("div#application")

VueRouter, VueJS, and Laravel route guard

I wanted to hide a particular page of my application behind a layer of security (a simple passcode form that will send a request to the server for validation).
Based off the documentation of VueRouter, I figured a beforeEnter would be appropriate. However, I am not entirely sure how one would require a user to access a particular component, and then successfully enter a passcode before being allowed to proceed to this current route.
Does anyone have an example of this? I am having trouble finding anything similar.
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter);
const routes = [
{ path: '/test/:testURL', component: require('./components/test.vue'),
beforeEnter: (to, from, next) => {
// somehow load another component that has a form
// the form will send a request to Laravel which will apply some middleware
// if the middleware successfully resolves, this current route should go forward.
}
},
];
const router = new VueRouter({
routes,
mode: 'history',
});
const app = new Vue({
router
}).$mount('#app');
Assuming you want to perform authentication only for selected components, you can go with using beforeEnter route guard. Use the following code.
const routes = [
{ path: '/test/:testURL', component: require('./components/test.vue'),
beforeEnter:requireLogin
},
];
function requireLogin(to, from, next) {
if (authenticated) {
next(true);
} else {
next({
path: '/login',
query: {
redirect: to.fullPath
}
})
}
}
Further, you can create a login screen and action in login component to redirect to given redirect parameter after setting authenticated variable to true. I recommend you to maintain authenticated variable in the veux store