Redirect to URL and append hash at the end - aurelia

I'm trying to redirect to a route/URL and append a hash at the end, but Aurelia keeps telling me ERROR [app-router] Error: Route not found: /#the-hash-i-added.
Here's the very basic code I'm using to redirect to a new route inside my authorize pipeline step:
return next.cancel(new Redirect(this.router.generate('home')));
This works fine and redirects non authorized users to the home page. However, I'd like them to end up at one specific part on the home page, hence the hash.
Now, since the Redirect class takes a URL as its argument, I figured I'd simply add the hash at the end, like so:
return next.cancel(new Redirect(this.router.generate('home') + '#hash-where-i-want-to-end-up'));
And while Aurelia successfully redirects to the correct URL (/#hash-where-i-want-to-end-up is set in the browser's URL bar) it still throws the aforementioned error and does not render the page.
I actually managed to hack around this by manually adding the hash after the redirect occurred, with a setTimeout:
setTimeout(() => {
window.location.hash = '#hash-where-i-want-to-end-up';
});
return next.cancel(new Redirect(this.router.generate('home')));
But not only does this feel like a dirty, dirty hack, it doesn't work well in other scenarios.
So, my question is, how can I redirect to a route and append a hash at the end?
Needless to say I'm using pushState: true and hashChange: false in my application.
Edit: I couldn't get a pushState app running on gist.run for some reason, but I set up a completely new Aurelia CLI project with this very basic setup:
app.js
import {inject} from 'aurelia-framework';
import {Router, Redirect} from 'aurelia-router';
export class App {
configureRouter (config, router) {
config.title = 'Test';
config.options.pushState = true;
config.options.hashChange = false;
config.options.root = '/';
config.map([
{
route: [''],
name: 'home',
moduleId: 'resources/elements/home',
title: 'Home'
},
{
route: ['about'],
name: 'about',
moduleId: 'resources/elements/about',
title: 'About',
settings: {auth: true}
},
]);
config.addAuthorizeStep(AuthorizeStep);
}
}
#inject(Router)
class AuthorizeStep {
constructor (router) {
this.router = router;
}
run (navigationInstruction, next) {
// Never allow access to auth routes
if (navigationInstruction.getAllInstructions().some(i => (i.config.settings && i.config.settings.auth))) {
return next.cancel(new Redirect(this.router.generate('home') + '#foo'));
}
return next();
}
}
app.html
<template>
<a route-href="route: home">Home</a> | <a route-href="route: about">About</a>
<router-view></router-view>
</template>
home.js
export class Home {}
home.html
<template>
<h1>Home</h1>
</template>
about.js
export class About {
}
about.html
<template>
<h1>About</h1>
</template>
As you can see, going to /about should redirect you back to /#foo, and while that indeed works (the user ends up at /#foo) I get the same error: vendor-bundle.js:14169 ERROR [app-router] Error: Route not found: /#foo

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.

Make vue router stay on same page if condition is not met

Im working on a vue app and I navigate through pages with vue router from the different page views, I want to set up vue router in a way where it would push a route if there is no error and stay on the same view/route if there is one. I have the following code that succeeds but I'm unsure if that is the best way to do it, and afraid it may cause bugs:
login() {
this.$store
.dispatch("method", {})
.then(() => {
if (!this.error) {
this.$router.push({ name: "nextPage" });
} else {
this.$router.push({ name: "samePage" });
}
});
}

Vue 3 with vue3-cookies sets same cookies to different locations on variable routes

I loading saved in cookies user authorization data when my Vue 3 application mount.
Data in json format like Jwt and Refresh tokens & something user data.
I have setup like:
main.js:
import VueCookies from 'vue3-cookies'
...
app.use(VueCookies, {
expireTimes: "365d",
path: "/"
})
In (vuex) store: auth.module.js:
I have things like this:
import { useCookies } from "vue3-cookies"
...
// in getters
const auth = cookies.get('auth')
// and in mutations
cookies.set('auth', auth)
And in router I have:
const routes = [
...
{
path: '/user/:id',
name: 'user',
props: true,
component: () => import('#/views/UserView.vue'),
meta: {
requiresAuth: true,
availableRoles: [ "Admin" ]
}
},
...
]
...
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
So, the problem is, that when I load the location like:
https://[url]/user/new
Or
https://[url]/user/1
I got duplicated cookies.
First auth cookie path is /user and its empty.
Second auth cookie path is / and it's ok.
Problem appears only on this route, and IDN why...
Seems like strange bug, do you have same issues?
If I going to route like:
<router-link to=`/user/{user.id}`>
Then issue occurs.
But, If I going to route like:
<router-link :to="{ name: 'user', params: { id: String(user?.id) } }">
That's fine.
Strange behavior.

Redirected when going from to via navigation guard

I'm trying to protect my Vue components using the Vue router authentication guard.
Case scenario: unauthenticated user lands on home page ("/" route) and he's trying to access "/profile", but that's a private component, so he'll be redirected by the vue router to "/auth/profile", so he'll authenticate and then the Auth component will redirect the user to the "/profile" component, because he got its path in the URL.
That's my guard
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.private)) {
if (!store.getters.getUser) {
//console.log("PRIVATE - NOT authenticated");
next({ path: "/auth" + `${to.path}` });
} else {
//console.log("PRIVATE - authenticated");
next();
}
} else {
//console.log("NOT PRIVATE");
next();
}
});
Everything works as expected, but I get an error and it's annoying
Redirected when going from "/" to "/profile" via a navigation guard.
Somewhere in your code, after being redirected to "/profile", you are being redirected back to "/". And that is what the vue-router is complaining about.
So the problem resides in being redirected multiple times per action.
You'll want to make sure you only have one redirect per navigation action.
problem solved by replacing
next({ name: "Onboarding" });
with
router.push({ path: 'Onboarding' });
Reduce vue-router version to 3.0.7, or
follow code into your app.js or index.js, which one you import vue-router
example:
import Vue from 'vue';
import VueRouter from 'vue-router';
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location, onResolve, onReject) {undefined
if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject)
return originalPush.call(this, location).catch(err => err)
}
Vue.use(VueRouter);
...
#code
This could be because your other component (the one you are pointing to) is redirecting you back.

How to return to view after authentication in Aurelia

I have a view that can be accessed by a direct link from an email.
Ex.
http://myServer:7747/#/pics/ClientId/YYYY-MM-DD
So this is set up using a route:
{ route: ['/pics', '/pics/:clientId/:sessionDate', 'pics'],
name: 'pics', moduleId: './views/pics', nav: false, title: 'Pictures',
auth: true, activationStrategy: activationStrategy.invokeLifecycle
},
So if a client clicks on this link and is not logged in, I want the view to redirect to a login screen (I am using aurelia-authentication plugin) and then when it succeeds, I want it to return to this page using the same urlParams.
I have the redirect to the login page working, but getting back to this view is proving difficult. If I just try to use history.back() the problem is that the authentication plugin has pushed another navigationInstruction (loginRedirect) onto the history before I can do anything. If I just try to hard-code a 'go back twice' navigation I run into a problem when a user simply tries to log in fresh from the main page and there is no history.
Seems like this should be easier than it is, what am I doing wrong?
I haven't used the aurelia-authentication plugin, but I can help with a basic technique you can use that makes this very easy. In your main.js file, set the root of your app to a "login" component. Within the login component, when the user has successfully authenticated, set the root of your app to a "shell" component (or any component you choose) that has a router view and configure the router in its view-model. Once this happens, the router will take the user to the proper component based on the url. If the user logs out, just set the app root back to the "login" component.
Here's some cursory code to attempt to convey the idea. I assume you're using the SpoonX plugin, but that's not really necessary. Just as long as you reset the root of your app when the user authenticates, it will work.
In main.js
.....
aurelia.start().then(() => aurelia.setRoot('login'));
.....
In login.js
import {AuthService} from 'aurelia-authentication';
import {Aurelia, inject} from 'aurelia-framework';
#inject(AuthService, Aurelia)
export class Login {
constructor(authService, aurelia) {
this.authService = authService;
this.aurelia = aurelia;
}
login(credentialsObject) {
return this.authService.login(credentialsObject)
.then(() => {
this.authenticated = this.authService.authenticated;
if (this.authenticated) {
this.aurelia.setRoot('shell');
}
});
}
.....
}
In shell.html
.....
<router-view></router-view>
.....
In shell.js
.....
configureRouter(config, router) {
this.router = router;
config.map(YOUR ROUTES HERE);
}
.....
I got this to work by replacing the plugin's authenticateStep with my own:
import { inject } from 'aurelia-dependency-injection';
import { Redirect } from 'aurelia-router';
import { AuthService } from "aurelia-authentication";
import { StateStore } from "./StateStore";
#inject(AuthService, StateStore)
export class SaveNavStep {
authService: AuthService;
commonState: StateStore;
constructor(authService: AuthService, commonState: StateStore) {
this.authService = authService;
this.commonState = commonState;
}
run(routingContext, next) {
const isLoggedIn = this.authService.authenticated;
const loginRoute = this.authService.config.loginRoute;
if (routingContext.getAllInstructions().some(route => route.config.auth === true)) {
if (!isLoggedIn) {
this.commonState.postLoginNavInstr = routingContext;
return next.cancel(new Redirect(loginRoute));
}
} else if (isLoggedIn && routingContext.getAllInstructions().some(route => route.fragment === loginRoute)) {
return next.cancel(new Redirect(this.authService.config.loginRedirect));
}
return next();
}
}
The only difference between mine and the stock one is that I inject a 'StateStore' object where I save the NavigationInstruction that requires authentication.
Then in my login viewModel, I inject this same StateStore (singleton) object and do something like this to log in:
login() {
var redirectUri = '#/defaultRedirectUri';
if (this.commonState.postLoginNavInstr) {
redirectUri = this.routing.router.generate(this.commonState.postLoginNavInstr.config.name,
this.commonState.postLoginNavInstr.params,
{ replace: true });
}
var credentials = {
username: this.userName,
password: this.password,
grant_type: "password"
};
this.routing.auth.login(credentials,
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } },
redirectUri
).catch(e => {
this.dialogService.open({
viewModel: InfoDialog,
model: ExceptionHelpers.exceptionToString(e)
});
});
};
Hope this helps someone!