Aurelia router not working when resetting root - authentication

I want to use aurelia-auth in my app and also have a login page that is completely separate from the rest of the app. I have been following the tutorial at this link: https://auth0.com/blog/2015/08/05/creating-your-first-aurelia-app-from-authentication-to-calling-an-api/
The problem I am having is after I successfully login and attempt to route to the app, none of the routes are found. I get route not found regardless of what I put in for the login redirect url.
Here is my code:
app.js:
...
import { Router } from 'aurelia-router';
import { AppRouterConfig } from './router-config';
import { FetchConfig } from 'aurelia-auth';
...
#inject(..., Router, AppRouterConfig, FetchConfig)
export class App {
constructor(router, appRouterConfig, FetchConfig) {
this.router = router;
this.appRouterConfig = appRouterConfig;
this.fetchConfig = fetchConfig;
...
}
activate() {
this.fetchConfig.configure();
this.appRouterConfig.configure();
}
...
}
login.js:
import { AuthService } from 'aurelia-auth';
import { Aurelia } from 'aurelia-framework';
...
#inject(..., Aurelia, AuthService)
export class LoginScreen {
constructor(..., aurelia, authService) {
this.aurelia = aurelia;
this.authService = authService;
...
}
login() {
return this.authService.login(this.username, this.password)
.then(response => {
console.log("Login response: " + response);
this.aurelia.setRoot('app');
})
.catch(error => {
this.loginError = error.response;
alert('login error = ' + error.response);
});
}
...
}
main.js:
import config from './auth-config';
import { AuthService } from 'aurelia-auth';
import { Aurelia } from 'aurelia-framework';
...
export function configure(aurelia) {
aurelia.use
.defaultBindingLanguage()
.defaultResources()
.developmentLogging()
.router()
.history()
.eventAggregator()
...
.plugin('aurelia-auth', (baseConfig) => {
baseConfig.configure(config);
});
let authService = aurelia.container.get(AuthService);
aurelia.start()
.then(a => {
if (authService.isAuthenticated()) {
a.setRoot('app');
} else {
a.setRoot('login');
}
});
}
auth-config.js:
var config = {
baseUrl: 'http://localhost:3001',
loginUrl: 'sessions/create',
tokenName: 'id_token',
//loginRedirect: '#/home' //looks like aurelia-auth defaults to #/ which is fine for me
}
export default config;
router-config.js:
import { AuthorizeStep } from 'aurelia-auth';
import { inject } from 'aurelia-framework';
import { Router } from 'aurelia-router';
#inject(Router)
export class AppRouterConfig {
constructor(router) {
this.router = router;
}
configure() {
console.log('about to configure router');
var appRouterConfig = function (config) {
config.title = 'My App';
config.addPipelineStep('authorize', AuthorizeStep);
config.map([
{
route: ['', 'home'],
name: 'home',
moduleId: '.components/home/home',
nav: true,
title: 'Home',
auth: true
},
{
route: ['employees'],
name: 'employees',
moduleId: './components/employees/employees',
nav: true,
title: 'Employees',
auth: true
}
]);
this.router.configure(appRouterConfig);
}
};
}
When loading the app, it successfully goes to login page and I'm able to successfully login and it tries to redirect, but I get this error in the console:
ERROR [app-router] Error: Route not found: /
at AppRouter._createNavigationInstruction (http://127.0.0.1:8080/jspm_packages/npm/aurelia-router#1.0.0-rc.1.0.1/aurelia-router.js:1039:29)
at AppRouter.loadUrl (http://127.0.0.1:8080/jspm_packages/npm/aurelia-router#1.0.0-rc.1.0.1/aurelia-router.js:1634:19)
at BrowserHistory._loadUrl (http://127.0.0.1:8080/jspm_packages/npm/aurelia-history-browser#1.0.0-rc.1.0.0/aurelia-history-browser.js:301:55)
at BrowserHistory.activate (http://127.0.0.1:8080/jspm_packages/npm/aurelia-history-browser#1.0.0-rc.1.0.0/aurelia-history-browser.js:200:21)
at AppRouter.activate (http://127.0.0.1:8080/jspm_packages/npm/aurelia-router#1.0.0-rc.1.0.1/aurelia-router.js:1689:20)
at eval (http://127.0.0.1:8080/jspm_packages/npm/aurelia-router#1.0.0-rc.1.0.1/aurelia-router.js:1670:21)
at AppRouter.registerViewPort (http://127.0.0.1:8080/jspm_packages/npm/aurelia-router#1.0.0-rc.1.0.1/aurelia-router.js:1672:10)
at new RouterView (http://127.0.0.1:8080/jspm_packages/npm/aurelia-templating-router#1.0.0-rc.1.0.1/router-view.js:112:19)
at Object.invokeWithDynamicDependencies (http://127.0.0.1:8080/jspm_packages/npm/aurelia-dependency-injection#1.0.0-rc.1.0.1/aurelia-dependency-injection.js:329:20)
at InvocationHandler.invoke (http://127.0.0.1:8080/jspm_packages/npm/aurelia-dependency-injection#1.0.0-rc.1.0.1/aurelia-dependency-injection.js:311:168)error # aurelia-logging-console.js:54log # aurelia-logging.js:37error # aurelia-logging.js:70(anonymous function) # aurelia-router.js:1637
aurelia-logging-console.js:54 ERROR [app-router] Router navigation failed, and no previous location could be restored.
I'm googling around quite a bit for answers to this, but having difficulty finding good answers. Anybody have any ideas? Any help is appreciated!

I think the problem is that you are configuring the router inside activate() method. In my opinion, there is no need to do this.
You can navigate to a route after resetting the root component:
this.aurelia.setRoot('./login')
.then((aurelia) => {
aurelia.root.viewModel.router.navigateToRoute('someLoginRoute');
});
You can also use the mapUnknownRoutes, which is very useful:
configureRouter(config, router) {
config.title = "Super Secret Project";
config.map([
{ route: [ '', 'screen-1'], moduleId: "./screen-1", nav: true, title: "Beginscherm" },
{ route: 'screen-2', name:'screen-2', moduleId: "./screen-2", nav: true, title: "Beginscherm" }
]);
config.mapUnknownRoutes(instruction => {
return './screen-1';
});
this.router = router;
}
See this example https://gist.run/?id=00b8b3745a480fb04184e8440e8be8c5. Pay attention at login/logout functions.
I hope this helps!

U can solve this by reloading or refreshing the app.
after setting app
ie.
a.setRoot('app');
location.reload();

Related

Trouble getting user data inside vue-router from composition

I'm very new to vue.js, I am currently working on my final assignment for university.
I'm trying to get information of my user into my router, this works fine on my usual pages/components, but the techniques used on those files don't seem to work here. I've tried reading through some of the documention for router and composition, but I can't seem to figure out where I'm going wrong. This is my latest attempt as earlier I was not using setup() and getting the error; inject() can only be used inside setup() or functional components.
The problem is occuring with "useAuth," I'm not getting any data, my console.log(isAdmin) is displaying 'undefined,' this should be a boolean true/false.
Router code:
import { createWebHistory, createRouter } from "vue-router";
import Dashboard from "../pages/DashboardSDT.vue";
import Events from "../pages/EventsSDT.vue";
import Results from "../pages/ResultsSDT.vue";
import Admin from "../pages/AdminSDT.vue";
import Settings from "../pages/SettingsSDT.vue";
import Login from "../pages/LoginSDT.vue";
import Register from "../pages/RegisterSDT.vue";
import { getAuth } from "firebase/auth";
import useAuth from "../composition/useAuth";
const routes = [
{
path: "/",
name: "Dashboard",
component: Dashboard
},
{
path: "/Events",
name: "Events",
component: Events
},
{
path: "/Results",
name: "Results",
component: Results
},
{
path: "/Admin",
name: "Admin",
component: Admin,
meta: { onlyAdminUser: true }
},
{
path: "/Settings",
name: "Settings",
component: Settings,
meta: { onlyAuthUser: true }
},
{
path: "/Login",
name: "Login",
component: Login,
meta: { onlyGuestUser: true }
},
{
path: "/Register",
name: "Register",
component: Register,
meta: { onlyGuestUser: true }
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
router.beforeEach((to, _, next) => {
const isAuth = !!getAuth().currentUser;
const isAdmin = useAuth.admin;
console.log(isAdmin)
if (to.meta.onlyAuthUser) {
if (isAuth) {
next()
} else {
next({ name: "Login" })
}
// } else if(to.meta.onlyAdminUser) {
// if(isAdmin) {
// next()
// }
// else {
// next({name: "BasicUser"})
// }
} else if (to.meta.onlyGuestUser) {
if (isAuth) {
next({ name: "Dashboard" })
} else {
next()
}
} else {
next()
}
})
export default {
setup() {
return {
...useAuth()
}
},
...router
}
useAuth code:
import { useStore } from 'vuex'
import { computed } from 'vue'
export default function useAuth() {
const store = useStore();
const { state } = store;
const error = computed(() => state.user.auth.error);
const isProcessing = computed(() => state.user.auth.isProcessing);
const isAuthenticated = computed(() => store.getters["user/isAuthenticated"]);
const user = computed(() => state.user.data);
const admin = computed(() => state.user.data.admin);
return {
error,
isProcessing,
isAuthenticated,
user,
admin
}
}
vue-router's index file is not like a vue component file and does not have a setup() function. I've never tried but it's unlikely you can use composable functions either, especially when using vue composition API functions like computed()
You can however import the vuex store and access all it's state, getters, etc. like you want.
import store from '/store/index.js'; // or wherever your store lives
Then inside your router guard
const isAuthenticated = store.getters["user/isAuthenticated"];
const isProcessing = store.state.user.auth.isProcessing
// ...etc

Vue Router fails to navigate from inside unit tests using jest

I have a router, Home, Login components and unit tests for the Login component.
The logic is: when user is unauthenticated, send him to Login page, once he's authenticated, send him to home page.
The logic works fine in the browser, however, when I run unit tests, I get an exception: thrown: undefined once the login component tries to navigate using this.$router.push('/');
In the console I see the message:
trying to route /login /
and then the exception is thrown once i run next();
Am I missing some setup to have the router working properly in the test environment?
Alternatively: is there a way to mock the next() function passed to the navigation guard?
Here's the router:
import VueRouter from 'vue-router';
import Home from '#/views/Home.vue';
import Login from '#/views/Login.vue';
import { state } from '#/store';
export const routes = [
{
path: '/',
name: 'home',
component: Home,
},
{
path: '/login',
name: 'login',
component: Login,
meta: {
noAuthRequired: true,
},
},
];
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes,
});
router.beforeEach((to: any, from: any, next: any) => {
console.log('trying to route', from.fullPath, to.fullPath);
const isAuthed = !!state.user.token;
if (!to.meta.noAuth && !isAuthed) {
next({ name: 'login' });
} else {
next();
}
});
export default router;
The component (relevant part):
import Vue from 'vue';
import Component from 'vue-class-component';
import { axios } from '../plugins/axios';
#Component
export default class App extends Vue {
private credentials = {
email: '',
password: '',
};
private error = '';
private async login() {
try {
const data = await axios.post('http://localhost:5000/api/v1/user/auth', this.credentials);
const token = data.data.payload;
this.$store.dispatch('setUser', { token });
this.error = '';
this.$router.push('/');
} catch (error) {
console.warn(error);
this.error = error;
}
}
}
And the unit test:
import Vue from 'vue';
import Vuetify from 'vuetify';
import AxiosMockAdapter from 'axios-mock-adapter';
import { Wrapper, shallowMount, createLocalVue } from '#vue/test-utils';
import flushPromises from 'flush-promises';
import Vuex, { Store } from 'vuex';
import { axios } from '#/plugins/axios';
import VTest from '#/plugins/directive-test';
import LoginPage from '#/views/Login.vue';
import { mainStore, state, IState } from '#/store';
import VueRouter from 'vue-router';
import router from '#/router';
describe('Login page tests', () => {
let page: Wrapper<Vue>;
let localStore: Store<IState>;
const localVue = createLocalVue();
const maxios = new AxiosMockAdapter(axios);
const vuetify = new Vuetify();
const errorMessage = 'Input payload validation failed';
const emailError = 'Invalid Email format';
const validData = {
email: 'valid#email.com',
password: 'test pass',
};
// in order for "click" action to submit the form,
// the v-btn component must be stubbed out with an HTML button
const VBtn = {
template: '<button type="submit"/>',
};
localVue.use(Vuetify);
localVue.directive('test', VTest);
localVue.use(Vuex);
localVue.use(VueRouter);
beforeAll(() => {
maxios.onPost().reply((body: any) => {
const jsonData = JSON.parse(body.data);
if (jsonData.email !== validData.email) {
return [400, {
message: errorMessage,
errors: { email: emailError },
}];
}
return [200, { payload: 'valid-token' }];
});
});
beforeEach(() => {
try {
localStore = new Vuex.Store({
...mainStore,
state: JSON.parse(JSON.stringify(state)),
});
page = shallowMount(LoginPage, {
store: localStore,
router,
localVue,
vuetify,
stubs: {
VBtn,
},
attachToDocument: true,
sync: false,
});
} catch (error) {
console.warn(error);
}
});
afterEach(() => {
maxios.resetHistory();
page.destroy();
});
const submitLoginForm = async (data: any) => {
page.find('[test-id="LoginEmailField"]').vm.$emit('input', data.email);
page.find('[test-id="LoginPasswordField"]').vm.$emit('input', data.password);
page.find('[test-id="LoginBtn"]').trigger('click');
await flushPromises();
};
it('Redirects user to home page after successful auth', async () => {
await submitLoginForm(validData);
expect(page.vm.$route.path).toEqual('/');
});
});

Angular 2 error after authentication - Cannot find primary outlet to load

I have an error in console after authentication. After reload page CreateChartComponent page start working. Error just happen in authentication process.
Uncaught (in promise): Error: Cannot find primary outlet to load 'CreateChartComponent'
This is the login function.
login(event, username, password): void {
event.preventDefault();
this.authService.login(username, password).subscribe(
res => {
this.router.navigate(['drawing']);
},
err => {
// todo: handle error with a lable
console.log(err);
if (err.ok === false) {
this.errorMessage = 'Error logging in.';
}
});
}
}
Aditional information:
I send clear mode of code where I get same issue.
It's Router code:
// Import our dependencies
import { Routes } from '#angular/router';
import { AppComponent } from './app.component';
import { LoginComponent } from './home/login/login.component';
import { CreateChartComponent } from './home/drawing/create-chart.component';
import { AuthGuard } from './auth.guard';
// Define which component should be loaded based on the current URL
export const routes: Routes = [
{ path: '', component: CreateChartComponent, pathMatch: 'full', canActivate: [AuthGuard] },
{ path: 'login', component: LoginComponent },
{ path: 'drawing', component: CreateChartComponent, canActivate: [AuthGuard] },
];
and its create-chart.component.ts
import {
Component,
OnInit,
} from '#angular/core';
#Component({
selector: 'np-chart-create',
templateUrl: './create-chart.component.html',
styleUrls: ['./create-chart.component.css']
})
export class CreateChartComponent implements OnInit {
ngOnInit() {
}
}

Vue-router dynamic load menu tree

I'm trying to create a menu tree with vue-router by ajax request,but the $mount function was called before the ajax request responsed, so the router in the Vue instance always null.
Is there any good solution here?
Here is my code (index.js):
import Vue from 'vue';
import Element from 'element-ui';
import entry from './App.vue';
import VueRouter from 'vue-router';
import VueResource from 'vue-resource';
import Vuex from 'vuex'
import configRouter from './route.config';
import SideNav from './components/side-nav';
import Css from './assets/styles/common.css';
import bus from './event-bus';
import dynamicRouterConfig from './dynamic.router';
Vue.use(VueRouter);
Vue.use(Element);
Vue.use(VueResource);
Vue.use(Vuex);
Vue.http.interceptors.push((request, next) => {
bus.$emit('toggleLoading');
next(() => {
bus.$emit('toggleLoading');
})
})
Vue.component('side-nav', SideNav);
app = new Vue({
afterMounted(){
console.info(123);
},
render: h => h(entry),
router: configRouter
});
app.$mount('#app');
route.config.js:
import navConfig from './nav.config';
import dynamicRouterConfig from './dynamic.router';
let route = [{
path: '/',
redirect: '/quickstart',
component: require('./pages/component.vue'),
children: []
}];
const registerRoute = (config) => {
//require(`./docs/zh-cn${page.path}.md`)
//require(`./docs/home.md`)
function addRoute(page) {
if (page.show == false) {
return false;
}
let component = page.path === '/changelog' ? require('./pages/changelog.vue') : require(`./views/alert.vue`);
if (page.path === '/edit') {
component = require('./views/edit.vue');
}
let com = component.default || component;
let child = {
path: page.path.slice(1),
meta: {
title: page.title || page.name,
description: page.description
},
component: com
};
route[0].children.push(child);
}
//if (config && config.length>0) {
config.map(nav => {
if (nav.groups) {
nav.groups.map(group => {
group.list.map(page => {
addRoute(page);
});
});
} else if (nav.children) {
nav.children.map(page => {
addRoute(page);
});
} else {
addRoute(nav);
}
});
//}
return { route, navs: config };
};
const myroute = registerRoute(navConfig);
let guideRoute = {
path: '/guide',
name: 'Guide',
redirect: '/guide/design',
component: require('./pages/guide.vue'),
children: [{
path: 'design',
name: 'Design',
component: require('./pages/design.vue')
}, {
path: 'nav',
name: 'Navigation',
component: require('./pages/nav.vue')
}]
};
let resourceRoute = {
path: '/resource',
name: 'Resource',
component: require('./pages/resource.vue')
};
let indexRoute = {
path: '/',
name: 'Home',
component: require('./pages/index.vue')
};
let dynaRoute = registerRoute(dynamicRouterConfig).route;
myroute.route = myroute.route.concat([indexRoute, guideRoute, resourceRoute]);
myroute.route.push({
path: '*',
component: require('./docs/home.md')
});
export const navs = myroute.navs;
export default myroute.route;
And dynamic.router.js:
module.exports = [
{
"name": "Edit",
"path": "/edit"
}
]
Now the static route config is woking fine ,but how can I load data from server side by ajax request in the route.config.js instead of static data.
Waiting for some async request at page render is fine, just set empty initial values in the data section of component like:
data() {
someStr: '',
someList: []
}
and make sure you handle the empty values well without undefined errors trying to read things like someList[0].foo.
Then when the request comes back, set the initially empty values to those real data you get from the request.
Giving the user some visual indicate that you're fetching the data would be a good practice. I've found v-loading in element-ui useful for that.

Aurelia - Switching between app roots with different route configurations

UPDATE
Could this issue have something to do with mu problem?
https://github.com/aurelia/framework/issues/400
I have an Aurelia application with two different roots, one for loggen in users, and another for anonymous users.
I have in other Aurelia apps implemented a chanaging of app root based on the approach in this answer. This works very well when the login module is an "isolated" module with no additional routes, but I'm having a hard time getting it to work now.
index.js - root for anonymous users
import {inject, useView, Aurelia} from "aurelia-framework";
import AuthService from "./services/auth-service";
#useView("app.html")
#inject(AuthService)
export class Index {
constructor(authService) {
this.auth = authService;
}
configureRouter(config, router) {
config.title = "Super Secret Project";
config.options.pushState = true;
config.map([
{ route: ["","home"], moduleId: "home", nav: true, title: "Beginscherm" },
{ route: "over", moduleId: "about", nav: true, title: "Over" },
{ route: "inloggen", moduleId: "account/login", nav: false, title: "Inloggen" }
]);
this.router = router;
}
}
ic-app.js - root for logged in users
import {useView, inject} from "aurelia-framework";
import {RequestStatusService} from "./services/request-status-service";
import AuthService from "./services/auth-service";
#useView("app.html")
#inject(RequestStatusService, AuthService)
export class App {
constructor(requestStatusService, authService) {
this.requestStatusService = requestStatusService;
this.auth = authService; // we use it to bind it to the nav-bar-top
}
configureRouter(config, router) {
config.title = "Super Secret Project";
config.options.pushState = true;
config.map([
{ route: ["", "selecteer-school"], moduleId: "ic/select-school", nav: false, title: "Selecteer School" },
{ route: "dashboard", moduleId: "ic/dashboard", nav: true, title: "Beginscherm" },
]);
this.router = router;
}
}
login code on auth-service.js
logIn(userData, rememberMe = false) {
this.requestStatusService.isRequesting = true;
return this.http
.fetch("/token", { method: "POST", body: userData })
.then((response) => response.json())
.then(response => {
if (response.access_token) {
this.setAccessToken(response.access_token, response.userName, rememberMe);
this.app.setRoot("ic-app");
}
});
}
and...
log off code in auth-service.js
logOff() {
AuthService.clearAccessToken();
this.app.setRoot("index");
}
The Problem
Setting the different app roots works as expected, the problem is that I would expect the new app root to automatically navigate to the default route of the new root, bit it tries to load the route it was on the moment setRoot(...) is called.
To illustrate with an example,
I'm on the login page. current route: /inloggen
I click the log in button. app.setRoot("ic-app") is called
New root is loaded; configureRouter in ic-app.js is called, and then...
Console error: Route not found: /inloggen
The new root tries to stay in the same /inloggen route, but I would like it to load, or navigate to, the default route for that app root.
The same happens on logging out.
How can I force the app to navigate to the default route after changing root?
I got this working great, I answered more about how to in this stackoverflow thread:
Aurelia clear route history when switching to other app using setRoot
Basically do the following
this.router.navigate('/', { replace: true, trigger: false });
this.router.reset();
this.router.deactivate();
this.aurelia.setRoot('app');
In the router for anonymous users use the mapUnknownRoutes. Like this:
configureRouter(config, router) {
config.title = "Super Secret Project";
config.options.pushState = true;
config.map([
{ route: ["","home"], moduleId: "home", nav: true, title: "Beginscherm" },
{ route: "over", moduleId: "about", nav: true, title: "Over" },
{ route: "inloggen", moduleId: "account/login", nav: false, title: "Inloggen" }
]);
config.mapUnknownRoutes(instruction => {
//check instruction.fragment
//return moduleId
return 'account/login'; //or home
});
this.router = router;
}
Do the same strategy in the other router. Now, try to logout and login again, you will see the user will be redirected to his last screen.
EDIT
Another solution is redirecting to desired route after setting the rootComponent. For instance:
logOut() {
this.aurelia.setRoot('./app')
.then((aurelia) => {
aurelia.root.viewModel.router.navigateToRoute('login');
});
}
Here is a running example https://gist.run/?id=323b64c7424f7bec9bda02fe2778f7fc
My best practice would be to simply direct them there, and this is what I do in my applications:
login(username, password) {
this.auth.login(username, password)
.then(() => {
this.aurelia.setRoot('app');
location.hash = '#/';
});
}