JPinchot
JPinchot

Reputation: 180

Hiding routes in Aurelia nav bar until authenticated

Is there a proper way to hide items in the Aurelia getting started app behind some authentication.

Right now I'm just adding a class to each element based on a custom property. This feels extremely hacky.

    <li repeat.for="row of router.navigation" class="${row.isActive ? 'active' : ''}${!row.isVisible ? 'navbar-hidden' : ''}">
      <a href.bind="row.href">${row.title}</a>
    </li>

Upvotes: 14

Views: 5863

Answers (4)

Paul
Paul

Reputation: 36319

I realize this is a bit of thread necromancy, but I wanted to add an answer because the accepted answer offers a solution that's explicitly recommended against by the Aurelia docs (you have to scroll down to the reset() method.

I tried several other methods, to varying degrees of success before I realized that I was looking at it wrong. Restriction of routes is a concern of the application, and so using the AuthorizeStep approach is definitely the way to go for blocking someone from going to a given route. Filtering out which routes a user sees on the navbar, though, is a viewmodel concern in my opinion. I didn't really feel like it was a value converter like @MickJuice did, though, as every example I saw of those were about formatting, not filtering, and also I felt like it's a bit cleaner / more intuitive to put it in the nav-bar view model. My approach was as follows:

// app.js
import AuthenticationService from './services/authentication';
import { inject } from 'aurelia-framework';
import { Redirect } from 'aurelia-router';

@inject(AuthenticationService)
export class App {
  constructor(auth) {
    this.auth = auth;
  }

  configureRouter(config, router) {
    config.title = 'RPSLS';
    const step = new AuthenticatedStep(this.auth);
    config.addAuthorizeStep(step);
    config.map([
      { route: ['', 'welcome'], name: 'welcome', moduleId: './welcome', nav: true, title: 'Welcome' },
      { route: 'teams', name: 'teams', moduleId: './my-teams', nav: true, title: 'Teams', settings: { auth: true } },
      { route: 'login', name: 'login', moduleId: './login', nav: false, title: 'Login' },
    ]);

    this.router = router;
  }
}

class AuthenticatedStep {
  constructor(auth) {
    this.auth = auth;
  }

  run(navigationInstruction, next) {
    if (navigationInstruction.getAllInstructions().some(i => i.config.settings.auth)) {
      if (!this.auth.currentUser) {
        return next.cancel(new Redirect('login'));
      }
    }

    return next();
  }
}

OK, so that by itself will restrict user access to routes if the user isn't logged in. I could easily extend that to something roles based, but I don't need to at this point. The nav-bar.html then is right out of the skeleton, but rather than binding the router directly in nav-bar.html I created nav-bar.js to use a full view-model, like so:

import { inject, bindable } from 'aurelia-framework';
import AuthenticationService from './services/authentication';

@inject(AuthenticationService)
export class NavBar {
  @bindable router = null;

  constructor(auth) {
    this.auth = auth;
  }

  get routes() {
    if (this.auth.currentUser) {
      return this.router.navigation;
    }
    return this.router.navigation.filter(r => !r.settings.auth);
  }
}

Rather than iterating over router.navigation at this point, nav-bar.html will iterate over the routes property I declared above:

<ul class="nav navbar-nav">
   <li repeat.for="row of routes" class="${row.isActive ? 'active' : ''}">
     <a data-toggle="collapse" data-target="#skeleton-navigation-navbar-collapse.in" href.bind="row.href">${row.title}</a>
   </li>
</ul>

Again, your mileage may vary, but I wanted to post this as I thought it was a fairly clean and painless solution to a common requirement.

Upvotes: 3

Matthew James Davis
Matthew James Davis

Reputation: 12295

These answers are great, though for the purposes of authentication, I don't think any have the security properties you want. For example, if you have a route /#/topsecret, hiding it will keep it out of the navbar but will not prevent a user from typing it in the URL.

Though it's technically a bit off topic, I think a much better practice is to use multiple shells as detailed in this answer: How to render different view structures in Aurelia?

The basic idea is to send the user to a login application on app startup, and then send them to the main app on login.

main.js

export function configure(aurelia) {
  aurelia.use
    .standardConfiguration()
    .developmentLogging();

  // notice that we are setting root to 'login'
  aurelia.start().then(app => app.setRoot('login'));
}

app.js

import { inject, Aurelia } from 'aurelia-framework';

@inject(Aurelia)
export class Login {
  constructor(aurelia) {
    this.aurelia = aurelia;
  }
  goToApp() {
    this.aurelia.setRoot('app');
  }
}

I've also written up an in-depth blog with examples on how to do this: http://davismj.me/blog/aurelia-login-best-practices-pt-1/

Upvotes: 8

MickJuice
MickJuice

Reputation: 539

Although I like PW Kad's solution (it just seems cleaner), here's an approach that I took using a custom valueConvertor:

nav-bar.html

<ul class="nav navbar-nav">
    <li repeat.for="row of router.navigation | authFilter: isLoggedIn" class="${row.isActive ? 'active' : ''}" >

      <a data-toggle="collapse" data-target="#bs-example-navbar-collapse-1.in" href.bind="row.href">${row.title}</a>
    </li>
  </ul>

nav-bar.js

import { bindable, inject, computedFrom} from 'aurelia-framework';
import {UserInfo} from './models/userInfo';

@inject(UserInfo)
export class NavBar {
@bindable router = null;

constructor(userInfo){
    this.userInfo = userInfo;
}

get isLoggedIn(){
    //userInfo is an object that is updated on authentication
    return this.userInfo.isLoggedIn;
}

}

authFilter.js

export class AuthFilterValueConverter {
toView(routes, isLoggedIn){
    console.log(isLoggedIn);
    if(isLoggedIn)
        return routes;

    return routes.filter(r => !r.config.auth);
}
}

Note the following:

  • Your isLoggedIn getter will be polled incessantly
  • You can achieve the same with an if.bind="!row.config.auth || $parent.isLoggedIn" binding, but make sure that your if.bind binding comes after your repeat.for

Upvotes: 3

PW Kad
PW Kad

Reputation: 14995

There are two directions you can take here.

The first is to only show nav links in the nav bar when the custom property is set like you are. To clean it up a bit let's use the show binding -

  <li repeat.for="row of router.navigation" show.bind="isVisible" class="${row.isActive ? 'active' : ''}">
    <a href.bind="row.href">${row.title}</a>
  </li>

The issue here is you still need to maintain the custom property like you are already doing. The alternative is to reset the router. This basically involves building out a set of routes that are available when the user is unauthenticated and then a separate set once the user is authenticated -

this.router.configure(unauthenticatedRoutes);
// user authenticates
this.router.reset();
this.router.configure(authenticatedRoutes);

This gives you the flexibility to reconfigure the router whenever you need to.

Upvotes: 18

Related Questions