Prevent route change on refresh with Vue navigation guards - vue.js

I have navigation guards working perfectly based on a userTp value, however whenever the user reloads a page it redirects them to name: 'login'.
I would like to prevent a route change on refresh. Is there a simple way to do this within navigation guards?
router.beforeEach(async (to, from, next) => {
if (to.meta.requireAuth) {
if (!store.state.auth.user) {
await store.dispatch(AUTH_REFRESH)
}
if (to.meta.userTp === store.state.auth.user.userTp) {
next()
window.scrollTo(0, 0)
} else {
next({ name: 'login' })
window.scrollTo(0, 0)
}
} else {
next()
window.scrollTo(0, 0)
}
})
EDIT:
I found a semi-workaround for this, but it's not perfect. It allows users to access pages if they know the correct page url path, but since my setup has a server-side userTp check before sending data it just shows the page template but without any data in it.
My temp solution:
(Added as the first if statement)
router.beforeEach(async (to, from, next) => {
if (to = from) {
if (!store.state.auth.user) {
await store.dispatch(AUTH_REFRESH)
to()
}
}

Related

Dynamically add a route in a Nuxt3 middleware

I have a Nuxt3 project where I'd like to add new routes based on an API call to a database. For example, let's say a user navigates to /my-product-1. A route middleware will look into the database and if it finds an entry, it will return that a product page should be rendered (instead of a category page, for example).
This is what I came up with:
export default defineNuxtPlugin(() => {
const router = useRouter()
addRouteMiddleware('routing', async (to) => {
if (to.path == '/my-awesome-product') {
router.addRoute({
component: () => import('/pages/product.vue'),
name: to.path,
path: to.path
})
console.log(router.hasRoute(to.path)) // returns TRUE
}
}, { global: true })
})
To keep it simple, I excluded the API call from this example. The solution above works, but not on initial load of the route. The route is indeed added to the Vue Router (even on the first visit), however, when I go directly to that route, it shows a 404 and only if I don't reload the page on the client does it show the correct page when navigated to it for the second time.
I guess it has something to do with the router not being updated... I found the following example in a GitHub issue, however, I can't get it to work in Nuxt3 as (as far as I'm aware) it doesn't provide the next() method.
When I tried adding router.replace(to.path) below the router.addRoute line, I ended up in an infinite redirect loop.
// from https://github.com/vuejs/vue-router/issues/3660
// You need to trigger a redirect to resolve again so it includes the newly added
route:
let hasAdded = false;
router.beforeEach((to, from, next) => {
if (!hasAdded && to.path === "/route3") {
router.addRoute(
{
path: "/route3",
name: "route3",
component: () => import("#/views/Route3.vue")
}
);
hasAdded = true;
next('/route3');
return;
}
next();
});
How could I fix this issue, please?
Edit:
Based on a suggestion, I tried using navigateTo() as a replacement for the next() method from Vue Router. This, however, also doesn't work on the first navigation to the route.
let dynamicPages: { path: string, type: string }[] = []
export default defineNuxtRouteMiddleware((to, _from) => {
const router = useRouter()
router.addRoute({
path: to.path,
name: to.path,
component: () => import ('/pages/[[dynamic]]/product.vue')
})
if (!dynamicPages.some(route => route.path === to.path)) {
dynamicPages.push({
path: to.path,
type: 'product'
})
return navigateTo(to.fullPath)
}
})
I also came up with this code (which works like I wanted), however, I don't know whether it is the best solution.
export default defineNuxtPlugin(() => {
const router = useRouter()
let routes = []
router.beforeEach(async (to, _from, next) => {
const pageType = await getPageType(to.path) // api call
if (isDynamicPage(pageType)) {
router.addRoute({
path: to.path,
name: to.path,
component: () => import(`/pages/[[dynamic]]/product.vue`),
})
if (!routes.some(route => route.path === to.path)) {
routes.push({
path: to.path,
type: pageType,
})
next(to.fullPath)
return
}
}
next()
})
})
I suggest you use dynamic routing within /page directory structure - https://nuxt.com/docs/guide/directory-structure/pages#dynamic-routes
The [slug] concept is designed exactly for your usecase. You don't need to know all possible routes in advance. You just provide a placeholder and Nuxt will take care of resolving during runtime.
If you insist on resolving method called before each route change, the Nuxt's replacement for next() method you're looking for is navigateTo
https://nuxt.com/docs/api/utils/navigate-to
And I advise you to use route middleware and put your logic into /middleware/routeGuard.global.ts. It will be auto-executed upon every route resolving event. The file will contain:
export default defineNuxtRouteMiddleware((to, from) => {
// your route-resolving logic you wanna perform
if ( /* navigation should happen */ {
return navigateTo( /* your dynamic route */ )
}
// otherwise do nothing - code will flow and given to.path route will be resolved
})
EDIT: However, this would still need content inside /pages directory or some routes created via Vue Router. Because otherwise navigateTo will fail, as there would be no route to go.
Here is an example of one possible approach:
https://stackblitz.com/edit/github-8wz4sj
Based on pageType returned from API Nuxt route guard can dynamically re-route the original URL to a specific slug page.

How to route to the route's children path in Vue

I'm trying to route to a different route in vue after clicking the back button in the browser. Below is what I have done:
beforeRouteLeave (to, from, next) {
if (to.path === '/place-ad') {
console.log("path is /place-ad")
next('/place-ad?ad_id=' + 12345)
} else {
console.log("path is I don't know")
next()
}
The problem with the above is that I end up with the error:
VueJs Maximum call stack size exceeded
If I now change the if statement check to to.path !== '/place-ad' then there is no error but it doesn't route me to where I want, in this case the else clause gets triggered. I've seen many other similar stackoverflow questions but from what I've seen they all want to reroute to a completely different route. Whereas I'm trying to route to the route's children path.
ie: /place-ad?ad_id=' + 12345 instead of just /place-ad. How do I achieve this? Thank you.
So you want to add a query param on a specific route, if it is not set?
You can also do it like this in your place-ad page component:
<script>
export default {
created () {
if (!this.$route.query.ad_id) {
this.$router.push({
query: {
ad_id: '12345',
...this.$route.query
}
})
}
}
}
</script>
Or, if you have to stick to the beforeRouteLeave, this one should work too:
beforeRouteLeave (to, from, next) {
if (to.path === '/place-ad') {
if (!to.query.ad_id) {
return next({
path: to.path,
query: {
...to.query,
ad_id: '12345'
}
})
}
}
return next()
}

Vue3 - Using beforeRouteEnter to prevent flashing content

I am using Vue with axios like
[...]
import VueAxios from "vue-axios";
import axios from "#/axios";
createApp(App)
.use(store)
.use(router)
.use(VueAxios, axios)
.mount("#app");
[...]
which works really great globally like this.axios... everywhere. I am also using Passport for authentification and in my protected route I would like to call my Express-endpoint .../api/is-authenticated to check if the user is logged in or not.
To make this work I would like to use the beforeRouteEnter-navigation guard, but unfortunately I can't call this in there.
Right now I am having in in the mounted-hook, which feels wrong. Is there any solution with keeping my code straight and clean?
I'd appreciate a hint. Thanks.
Edit: This worked for me.
beforeRouteEnter(to, from, next) {
next((vm) => {
var self = vm;
self
.axios({ method: "get", url: "authenticate" })
.then() //nothing needed here to continue?
.catch((error) => {
switch (error.response.status) {
case 401: {
return { name: "Authentification" }; //redirect
//break;
}
default: {
return false; //abort navigation
//break;
}
}
});
});
With beforeRouteEnter there is access to the component instance by passing a callback to next. So instead of this.axios, use the following:
beforeRouteEnter (to, from, next) {
next(vm => {
console.log(vm.axios); // `vm` is the instance
})
}
Here's a pseudo-example with an async request. I prefer async/await syntax but this will make it clearer what's happening:
beforeRouteEnter(to, from, next) {
const url = 'https://jsonplaceholder.typicode.com/posts';
// ✅ Routing has not changed at all yet, still looking at last view
axios.get(url).then(res => {
// ✅ async request complete, still haven't changed views
// Now test the result in some way
if (res.data.length > 10) {
// Passed the test. Carry on with routing
next(vm => {
vm.myData = res.data; // Setting a property before the component loads
})
} else {
// Didn't like the result, redirect
next('/')
}
})
}

vue-router next method not working always in beforeach

i'm creating a navigation guard for redirect user to some page,
i get auth status from vuex:
state: {
auth: false,
},
and in vue-router beforeach, i set a condition that when auth state is false and route is not adminAuth,
redirect to adminAuth route
var auth = store.state.auth
if (!auth){
if( to.name !== "adminAuth" ){
next( { name: 'adminAuth' } )
}
}
problem is when route changed first time, next() not working properly, but second time, working properly!
can you help me?
You want to intercept the navigation request using beforeResolve or beforeEnter because beforeEach is too soon and some attributes of the logic you want to use have not been processed yet, so it is possible on second navigation that some underlying logic appears resolved because the next request is accessing something set by the previous request.
const router = new VueRouter({
routes: { ... }
})
router.beforeResolve((to, from, next) => {
if(!router.app.$store.state.auth && to.name !== 'adminAuth') {
next( { name: 'adminAuth' } )
}
next()
})

vue router next() function don't work in Promise in router.beforeEach

I have the following code:
router.beforeEach((to, from, next) => {
if (to.name !== from.name) {
store
.dispatch("fetchCurrentUser")
.then(() => {
console.log('then');
// do something
next();
})
.catch(() => {
console.log('catch');
router.push("/login");
next();
});
} else {
next();
}
// next();
});
I'm trying to get the current user, and if this succeeds, then do something with this data, and if the request is not successful, then redirect the user to the login page. But next () calls do not work, I get the "then" or "catch" in the console, but the redirect does not occur and an infinite loop begins. But if I take next () from condition (commented row) the redirect works fine.
To redirect you should use next('/') or next({ path: '/' }).
From the documentation:
next: Function: this function must be called to resolve the hook. The
action depends on the arguments provided to next:
next(): move on to the next hook in the pipeline. If no hooks are
left, the navigation is confirmed.
next(false): abort the current navigation. If the browser URL was
changed (either manually by the user or via back button), it will be
reset to that of the from route.
next('/') or next({ path: '/' }): redirect to a different location.
The current navigation will be aborted and a new one will be started.
You can pass any location object to next, which allows you to specify
options like replace: true, name: 'home' and any option used in
router-link's to prop or router.push
The promise resolves after the function ends.
This means that the commented next happens regardless of the result of the promise result. Then the promise resolves and you call another next.
The bottom line is that you don't need the commented next and should just cover the promise resolve.
I was able to implement an async validation inside beforeEach, authentication in my case.
export async function onBeforeEach(to, from, next) {
let { someUserToken } = to.query
if (someUserToken) {
let nextRoute = await authenticate(to)
next(nextRoute)
} else {
const userToken = store.state.application.userToken
if (!to.meta.public && !userToken) {
next({ name: 'Forbidden' })
} else {
next()
}
}
}
async function authenticate(to) {
let { someUserToken, ...rest } = to.query
return store
.dispatch('application/authenticate', {
someUserToken
})
.then(() => {
return {
name: 'Home',
query: {
...rest
}
}
})
.catch(() => {
return { name: 'Forbidden' }
})
}
I hope this helps.