Aurelia Router: Good pattern for authorizing child routes - aurelia

I'd like to authorize routes on a child router, but I'm not super happy with the current solution that I have. My current routes look like the following:
/
/account
/account/signin
/account/signout
To implement this, I've defined two routers - a root router and a child router for the account pages. To authorize the routes on my account child router, I added the following pipeline step to my root router config.
config.addAuthorizeStep({
run: (instruction: NavigationInstruction, next: Next): Promise<any> => {
if (!this.auth.isAuthenticated && (instruction.fragment === '/account' || instruction.fragment === '/account/profile')) {
return next.cancel(new Redirect('account/signin'))
}
if (this.auth.isAuthenticated && instruction.fragment === '/account/signin') {
return next.cancel(new Redirect('account'))
}
if (this.auth.isAuthenticated && instruction.fragment === '/account/signup') {
return next.cancel(new Redirect('account'))
}
return next();
}
})
It works, but I feel like there has to be a better way... I'd really like to accomplish the following:
Move the authorize logic to the account child router.
Use route names instead of fragments as it seems more robust

You can add auth property to route config and check for that one
http://aurelia.io/docs/routing/configuration#pipelines
import {Redirect} from 'aurelia-router';
export class App {
configureRouter(config) {
config.addAuthorizeStep(AuthorizeStep);
config.map([
{ route: ['', 'home'], name: 'home', moduleId: 'home/index' },
{ route: 'users', name: 'users', moduleId: 'users/index', settings: { auth: true } },
{ route: 'users/:id/detail', name: 'userDetail', moduleId: 'users/detail', settings: { auth: true } }
]);
}
}
class AuthorizeStep {
run(navigationInstruction, next) {
if (navigationInstruction.getAllInstructions().some(i => i.config.settings.auth)) {
var isLoggedIn = // insert magic here;
if (!isLoggedIn) {
return next.cancel(new Redirect('login'));
}
}
return next();
}
}

Related

Vue-Router beforeEnter not function as expected?

I am trying to protect a route using beforeEnter. my route looks like such;
path: '/account',
name: 'account',
component: Account,
beforeEnter:
(to, from, next) => {
const authService = getInstance();
const fn = () => {
// If the user is authenticated, continue
if (authService.isAuthenticated) {
console.log('no')
return next();
}
// Otherwise, log in
console.log('should login')
authService.loginWithRedirect({ appState: { targetUrl: to.fullPath } });
};
if (!authService.loading) {
return fn();
}
authService.$watch("loading", loading => {
if (loading === false) {
return fn();
}
})
}
},
THIS functions as I expect, but I don't believe the logic should into the routes file, so simply enough I store it in a different file under my auth folder. Like so;
import { getInstance } from "./index";
export const authGuard = (to, from, next) => {
console.log('test')
const authService = getInstance();
const fn = () => {
// If the user is authenticated, continue with the route
if (authService.isAuthenticated) {
console.log('no')
return next();
}
// Otherwise, log in
console.log('should login')
authService.loginWithRedirect({ appState: { targetUrl: to.fullPath } });
};
// If loading has already finished, check our auth state using `fn()`
if (!authService.loading) {
return fn();
}
// Watch for the loading property to change before we check isAuthenticated
authService.$watch("loading", loading => {
if (loading === false) {
return fn();
}
});
};
However when I import this to my routes and do;
import { authGaurd } from './auth/authGaurd'
Vue.use(Router)
export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/account',
name: 'account',
component: Account,
beforeEnter: authGaurd
},
this no long works? I'm sure I must be missing something simple? Any help would be much appreciated.
Try:
- beforeEnter: authGaurd
+ beforeEnter(to, from, next) {
authGaurd(to, from, next)
}

How to create a middleware or how to manage the below route for front end and admin

How can I manage the url for front and admin panel Via Middleware in Vue.
This is the code I have written in router/index.js file:
const router = new VueRouter({ mode: 'history', routes });
router.beforeEach((to, from, next) => {
// redirect to login page if not logged in and trying to access a restricted page
const loggedIn = localStorage.getItem('usertoken') == null ? false : true;
const user = JSON.parse(localStorage.getItem('user'));
//this is for admin
next('/admin/login')
next('/admin/home');
//this is my front URL
next('/terms-condition');
next('/home');
next()
})
export default router;
See the below code it may helps you
/**
* middleware for authentication
*/
router.beforeEach((to, from, next) => {
// redirect to login page if not logged in and trying to access a restricted page
const loggedIn = localStorage.getItem('usertoken') == null ? false : true;
const user = JSON.parse(localStorage.getItem('user'));
if (to.meta.portal == 'admin') {
if (to.meta.auth) {
if (!loggedIn) {
next('/admin/login')
} else if (loggedIn) {
next();
}
} else {
if (!loggedIn) {
next();
} else if (loggedIn) {
if (user.role_id == '1') {
next('/admin/home');
} else {
next('/');
}
}
}
} else if (to.meta.portal == 'front') {
if (loggedIn) {
if (user.role_id == '1') {
next('/admin/home');
} else {
next('/');
}
} else if (!loggedIn) {
if (to.path == "/admin") {
next('/admin/login');
} else {
next();
}
}
}
next()
})
export default router;
And you need to create two router files one for front and other for admin:
//front route file will look like
export default [{
path: '/',
meta: { auth: false, portal: 'front' },
component: () => import('#/components/layouts/front/main.vue'),
children: [
{
path: '/',
name: 'front-home',
title: 'Dashboard',
meta: { auth: false, portal: 'front' },
}
]
}]
//admin router file will be like
export default [
{
path: 'user',
name: 'users',
title: 'Users',
meta: { auth: true, portal: 'admin' },
component: () => import('#/components/templates/admin/user'),
}
]
Main difference is the portal that defines which portal will access by the respective route.Without portal inside meta it won't work.
The way you have implemented is correct
Once the user is successfully logged in , use if else condition to redirect to admin panel, also use Navigation guards given in vue-router
https://router.vuejs.org/guide/advanced/navigation-guards.html#per-route-guard
This help to prevent the other user to use this url directly

How to get something to run before vue-router

I am building my first SPA and I am facing some issues. This is how it is designed:
Laravel & Laravel Views handle the login and registration related pages.
SPA starts at the user logged in page.
My app.js defines a default VueJS app in which I use the mounted() method to set the state (VUEX) of the logged in user. Ideally, all it does is get the user details via an axios call to the Laravel backend and populate the VUEX state.
I use beforeEnter() methods in the route definitions to ensure only authorized people can navigate to the route.
This is where I face the problem. When a user logs in, it seems like router is executed before the vuex is set. Say I have a url /dashboard and /user/1. When I try to go to user/1 it works perfectly if it is after I load the application. But, if I refresh the webpage when I am in user/1, then router beforeEnter cannot find vuex state of the user so it redirects user to dashboard. This is because when the router runs beforeEnter, if it is a fresh page load, it wouldn't have access to the user Vuex state or it has access, but the value isn't set yet.
Because of this my biggest problem is I can't link to a route page directly since it always lands in dashboard and then the user will have to go to the route for it to work. How can I handle this situation?
This is what I ended up doing. I defined a function with which I initialized Vue.
At the end of the app.js I used Axios to retrieve the current user via Ajax. In the then method of the promise, I set the store with the user details I received in the promise and then call the function that I defined for Vue initialization above. This way when vue is initialized the store user already has the data.
The code change was very minimal and I didn't have to change the existing axios implementation.
This is my new implementation:
Axios.get('/api/user/info')
.then(response => {
(new Vue).$store.commit('setUser', response.data);
initializeVue();
})
.catch(error => initializeVue());
function initializeVue()
{
window.app = new Vue({
el: '#app',
router,
components: {
UserCard,
Sidebar,
},
methods: mapMutations(['setUser']),
computed: mapState(['user']),
});
}
I use $root as a bus and turn to VueX as a last resort, Here is some code i have stripped out of a plugin i am working on, I have adapted it slightly for you to just drop in to your code.., Should get you going.
This configuration supports VUE Cli.
Don't worry about session expiery, an interceptor will watching for a 401 response from Laravel will do to prompt the user to re-authenticate.
Ditch the axios configuration in bootstrap.js and replace it with this setup and configure Access-Control-Allow-Origin, the wildcard will do for local dev.
axios.defaults.withCredentials = true;
axios.defaults.headers.common = {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': undefined,
'Access-Control-Allow-Origin': '*'
};
axios.interceptors.response.use(
function (response) {
if(response.headers.hasOwnProperty('x-csrf-token')) {
axios.defaults.headers['X-CSRF-TOKEN'] = response.headers['x-csrf-token'];
}
return response;
},
function (error) {
if(typeof error !== 'object' || !error.response) {
return Promise.reject(error);
}
if(error.response.hasOwnProperty('status')) {
switch(error.response.status) {
case 401:
case 419:
// DO RE-AUTHENTICATE CALL
break;
}
}
return Promise.reject(error);
}
);
For the rest...
In main.js
data() {
return {
user: {},
authenticating: false
}
},
computed: {
isAuthenticated() {
// Check a credential only an authorized user would have.
if(this.$router.app.hasOwnProperty('user') === false || this.$router.app.user === null) {
return false;
}
return this.$router.app.user.hasOwnProperty('id');
}
},
methods: {
checkAuth: function () {
this.$set(this.$router.app, 'authenticating', true);
axios.get('/auth/user').then(response => {
this.$set(this.$router.app, 'user', response.data.user);
if (this.$router.app.isAuthenticated()) {
this.$router.push(this.$router.currentRoute.query.redirect || '/', () => {
this.$set(this.$router.app, 'authenticating', false);
});
}
}).catch(error => {
// TODO Handle error response
console.error(error);
this.$set(this.$router.app, 'user', {});
}).finally(() => {
this.$set(this.$router.app, 'authenticating', false);
});
},
login: function (input) {
axios.post('/login', input).then(response => {
this.$set(this.$router.app, 'user', response.data.user);
this.$router.push(this.$router.currentRoute.query.redirect || '/');
}).catch(error => {
// TODO Handle errors
console.error(error);
});
},
logout: function () {
axios.post('/logout').then(response => {
this.$set(this.$router.app, 'user', {});
this.$nextTick(() => {
window.location.href = '/';
});
}).catch(error => {
console.error(error);
});
},
}
beforeCreate: function () {
this.$router.beforeResolve((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth) && !this.$router.app.isAuthenticated()) {
next({
name: 'login',
query: {
redirect: to.fullPath
}
});
return;
}
next();
});
}
In Auth/LoginController.php add method
public final function authenticated(Request $request)
{
return response()->json([
'user' => Auth::user()
]);
}
Create app/Http/Middleware/AfterMiddleware.php
It will pass back a new CSRF token only when it changes rather than on every request. The axios interceptor will ingest the new token when detected.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Cookie;
class AfterMiddleware
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if(Cookie::get('X-CSRF-TOKEN',false) !== csrf_token())
return $next($request)->header('X-CSRF-TOKEN',csrf_token());
return $next($request);
}
}
You effectively can replace the static login form with a Vue login form with this setup.
Here is what router setup looks like:
new Router({
mode: 'history',
routes: [
{
path: '/login',
name: 'login',
component: AuthLogin,
meta: {
requiresAuth: false,
layout: 'auth'
}
},
{
path: '/login/recover',
name: 'login-recover',
component: AuthLoginRecover,
meta: {
requiresAuth: false,
layout: 'auth'
}
},
{
path: '/',
name: 'index',
component: Dashboard,
meta: {
requiresAuth: true,
layout: 'default'
}
},
{
path: '/settings',
name: 'settings',
component: Settings,
meta: {
requiresAuth: true,
layout: 'default'
}
}
]
});

vuejs - Redirect from login/register to home if already loggedin, Redirect from other pages to login if not loggedin in vue-router

I want to redirect user to home page if logged-in and wants to access login/register page and redirect user to login page if not logged-in and wants to access other pages. Some pages are excluded that means there is no need to be logged in so my code is right below:
router.beforeEach((to, from, next) => {
if(
to.name == 'About' ||
to.name == 'ContactUs' ||
to.name == 'Terms'
)
{
next();
}
else
{
axios.get('/already-loggedin')
.then(response => {
// response.data is true or false
if(response.data)
{
if(to.name == 'Login' || to.name == 'Register')
{
next({name: 'Home'});
}
else
{
next();
}
}
else
{
next({name: 'Login'});
}
})
.catch(error => {
// console.log(error);
});
}
});
But the problem is that it gets into an infinite loop and for example each time login page loads and user not logged-in, it redirects user to login page and again to login page and...
How can I fix this?
Here's what I'm doing. First I'm using a meta data for the routes, so I don't need to manually put all routes that are not requiring login:
routes: [
{
name: 'About' // + path, component, etc
},
{
name: 'Dashboard', // + path, component, etc
meta: {
requiresAuth: true
}
}
]
Then, I have a global guard like this:
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
// this route requires auth, check if logged in
// if not, redirect to login page.
if (!store.getters.isLoggedIn) {
next({ name: 'Login' })
} else {
next() // go to wherever I'm going
}
} else {
next() // does not require auth, make sure to always call next()!
}
})
Here I am storing if the user is logged in or not, and not making a new request.
In your example, you have forgotten to include Login into the list of pages that "don't need authentication". So if the user is trying to go to let's say Dashboard, you make the request, turns out he's not logged in. Then he goes to Login, BUT your code checks, sees it's not part of the 3 "auth not required" list, and makes another call :)
Therefore skipping this "list" is crucial! ;)
Good luck!
If someone is still looking for an answer, you can reverse the logic. So, the same way you have requiresAuth, you will have hidden routes for authenticated users. (example with firebase)
routes: [{
path: '/',
redirect: '/login',
meta: {
hideForAuth: true
}
},
{
path: '/dashboard',
name: 'dashboard',
component: Dashboard,
meta: {
requiresAuth: true
}
}]
And in your beforeEeach
router.beforeEach((to, from, next) => {
firebase.auth().onAuthStateChanged(function(user) {
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!user) {
next({ path: '/login' });
} else {
next();
}
} else {
next();
}
if (to.matched.some(record => record.meta.hideForAuth)) {
if (user) {
next({ path: '/dashboard' });
} else {
next();
}
} else {
next();
}
});
});
Derived from the answer #Andrey Popov provided.
I prefer to explicitly disallow a route that doesn't require auth. This prevents accidentally not protecting your routes, i.e. the default case is to redirect to a login page
router.beforeEach((to, from, next) => {
if (to.name === 'login') {
next() // login route is always okay (we could use the requires auth flag below). prevent a redirect loop
} else if (to.meta && to.meta.requiresAuth === false) {
next() // requires auth is explicitly set to false
} else if (store.getters.isLoggedIn) {
next() // i'm logged in. carry on
} else {
next({ name: 'login' }) // always put your redirect as the default case
}
})
In addition to the Andrey's answer, if you use firebase authentication, need to add onAuthStateChanged around createApp in main.ts.
firebase.auth().onAuthStateChanged((user) => {
createApp(App).use(store).use(router).mount('#app')
})
This is very basic concept for this, use redirect:'/dashboard' this way you can do. you have to define it in your route list. like this way. you can ignore mate: {}. i used this for different purpose.
routes: [ ....
{
path: '/login',
name: 'login',
component: LoginView,
meta:{needAuth:false},
redirect:'/dashboard'
},
{
path: '/sign-up',
name: 'sign-up',
component: SignUpView,
meta:{needAuth:false},
redirect:'/dashboard'
},
{
path: '/dashboard',
name: 'dashboard',
component: UserDashboard,
meta:{needAuth:true},
beforeEnter: ifAuthenticated,
}
]
function ifAuthenticated (to, from, next) { store.test();
if (localStorage.getItem("login-token")) { console.log("login done");
next();
return;
}
router.push({ name: 'login' });
};
// requireAuth for those you want to authenticate
// onlyGuest for those you don't want to autenticate to open other thing if already
// logged in then it's redirect to home
router.beforeEach((to,from,next)=>{
if(to.matched.some(record => record.meta.requireAuth)){
if(!store.state.loginUser){
next({name:'Login'});
}
else{
next();
}
}
else if (to.matched.some(record => record.meta.onlyGuest)) {
if (store.state.loginUser && store.state.loginUser.token) {
next({name: "Home"});
} else {
next();
}
}
else{
next();
}
});

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 = '#/';
});
}