Hiding routes in Aurelia nav bar until authenticated - aurelia

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>

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.

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/

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

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.

Related

Storybook: Clicking a button redirects user

I have a link component that I want to display in Storybook. I'm using Vue.
This is the component
NavigationLink.vue:
<ul>
<li :class="{selected: isSelected}">
<a :href="linkUrl">
<i :class="'isax isax-' + linkIcon"></i>
{{ linkName }}
</a>
</li>
</ul>
This is the story NavigationLink.stories.js:
import NavigationLink from '../app/javascript/components/NavigationLink.vue'
export default {
title: 'Navigation/Links',
component: NavigationLink,
argTypes: { ...argTypes here... }
}
const Template = (args) => ({
components: { NavigationLink },
setup() {
return { args };
},
template: '<NavigationLink v-bind="args"></NavigationLink>',
});
export const UnselectedLink = Template.bind({});
UnselectedLink.args = { ...args here... }
export const SelectedLink = Template.bind({});
SelectedLink.args = { ...args here... }
Unfortunately, you're still able to 'click' the link in Storybook and this then redirects to the 'Introduction' page within Storybook. But I don't want it to redirect there at all. In fact, if possible, I'd prefer to not be able to click it at all.
Any suggestions? Thanks in advance.
In the end I couldn't find a fancy way of doing this, so I just set the :href prop to be linkHref: '/?path=/story/navigation-links--unselected-link' in the args for UnselectedLink (and then similar for the SelectedLink). This still allows for the link to be clicked but will just redirect you back to the same story within Storybook.

Accessing another model-view from a page inside the main router

I’m trying to find out a way to creating a custom view-model and making its functions accessible from a page within the main router-view element. This custom view-model, in my case called "secondbar" is supposed to be located under main nav-bar and should contain a login status ("Logged in as ..." / "Not logged in").
In my router-view, one of the pages is a login page. After successful login, I want to be able to call a function of "secondbar" directly in order to change the login status there without page refresh.
I tried to inject "secondbar" class in login.js file; this way I can access the functions, but the message on the page wouldn't change (it seems like I'm accessing another instance of "secondbar"). I also tried to print out the same message directly on the main nav-bar, but it seems like this is not the right approach and it didn’t work either.
Is there some way, how I can access a "secondbar" class directly (the same instance is being shown in the browser) and call a function located there from a page inside a router-view?
App.html
<template bindable="router">
<require from="secondbar/secondbar"></require>
<!-- navbar -->
<secondbar view-model.ref="secondbar"></secondbar>
<router-view>
<!—- page content -->
</router-view>
</template>
App.js
import {Redirect} from 'aurelia-router';
import {inject} from 'aurelia-framework';
export class App {
configureRouter(config, router) {
this.router = router;
config.title = ‘’;
config.map([
{ route: [''], name: 'home', moduleId: 'home/index', nav: true, title: 'Home' , settings: { roles: [''] }},
{ route: 'login', name: 'login', moduleId: 'login/login', nav: true, title: 'Log In', settings: { roles: [''] }},
]);
}
}
What you could do is create a login-service or something similar, where you can bind to in both the secondbar and the login page-view. The basic would look like this:
login-page:
import login-service from "/login-service/login-service";
// inject
login() {
// do login stuff
this.loginService.currentUser = user; // user would be any object of choice
}
secondbar.js:
import login-service from "/login-service/login-service";
// inject
secondbar.html
<span if.bind="loginService.currentUser">
Logged in as: ${loginService.currentUser.username}
</span>
<span if.bind="!loginService.currentUser">
Not logged in
</span>

Experiencing navbar flicker with Vue.js

I'm experiencing a flicker in my navbar before a function is evaluated to either true or false.
The function that needs to evaluate is the following:
export default {
methods: {
isAuthenticated () {
return this.$store.state.user.authenticated
}
},
data: () => {
return {
unauthenticated: [
{
title: 'Link1',
url: '/link1'
},
{
title: 'Link2',
url: '/link2'
},
{
title: 'Link3',
url: '/link3'
}
],
authenticated: [
{
title: 'otherLink1',
url: '/otherlink1'
},
{
title: 'otherLink2',
url: '/otherlink2'
},
{
title: 'otherLink3',
url: '/otherlink3'
}
]
}
}
}
And the navbar has the following:
<template v-if="isAuthenticated()">
<b-nav is-nav-bar>
<b-nav-item v-for="nav in authenticated" :key="nav.title" :href="nav.url">{{nav.title}}</b-nav-item>
</b-nav>
</template>
<template v-else>
<b-nav is-nav-bar>
<b-nav-item v-for="nav in unauthenticated" :key="nav.title" :href="nav.url">{{nav.title}}</b-nav-item>
</b-nav>
</template>
However, when I click through the navigation, the unauthenticated links appear for a second and then the authenticated links appear as if the isAuthenticated() function hasn't evaluated yet. What can I do to remove this flicker?
My store file (user.js) file looks like this:
export const state = () => ({
headers: {},
profile: {}
})
export const mutations = {
updateHeaders (state, headers) {
state.headers.access_token = headers['access-token']
state.headers.token_type = headers['token-type']
state.headers.client = headers['client']
state.headers.expiry = headers['expiry']
state.headers.uid = headers['uid']
if (state.headers.expiry == null) {
state.authenticated = false
} else {
let timeToExpiry = new Date(state.headers.expiry * 1000)
let now = new Date()
state.authenticated = now < timeToExpiry
}
},
signout (state) {
state.headers = {}
state.profile = {}
}
}
The login/logout methods occur via API calls to a Rails app. The Devise gem handles the rest.
Thanks in advance!
EDIT:
I am using Nuxt.js for the layouts/pages/components so I believe that links submit with a this.$router.push(url) under the hood.
The b-nav tags are coming from Bootstrap Vue
When using bootstrap-vue there are two ways to add links to the navbar. One is to bind to :href attribute, which creates a regular html anchor. The other is to use :to attribute, which creates a link that interacts with vue-router.
<b-navbar-nav v-if="isAuthenticated()">
<b-nav-item v-for="nav in authenticated" :key="nav.title" :to="nav.url">{{nav.title}}</b-nav-item>
</b-navbar-nav>
<b-navbar-nav v-if="!isAuthenticated()">
<b-nav-item v-for="nav in unauthenticated" :key="nav.title" :to="nav.url">{{nav.title}}</b-nav-item>
</b-navbar-nav>
No reason to use <template> tags here to encapsulate the . Also note that 'is-nav-bar' is deprecated. See here where they note the deprecation.
What code executes when you click one of the links is not stated, I assume it's something like this.$router.push(url). If this is the case, you've probably have included your navbar in the <router-view>, so when you switch current route, components inside <router-view> rerender, so the navbar flashes. Move them out of the <router-view> should fix this.
edit: so the OP is not using vue-router yet, in this case, either manually change the root component's data to make parts other than the navs change, or add vue-router and use this.$router.push() to navigate so parts outside <router-view> won't change or flash.
Anyway, we need the vue component to stay to let vue to rerender only part of the view, while simply navigating by <a> or something will destruct everything and reconstruct them again, hence the flashing.

How can Aurelia child routing work with a parameter?

I feel as though similar questions have already been asked, but I have been unable to find my answer.
I'm trying to segregate my application by its features. Ideally each feature would be able to setup its own routing as well and Aurelia's child-router functionality seemed to be the perfect fit, but I'm having trouble getting it to work.
The structure of the application is as such:
app.ts
app.html
/lectures
list.ts
list.html
details.ts
details.html
index.ts
index.html
I can include any of the other files if needed to answer the question, but have tried to keep the question as compact as possible. The app.html and lectures/index.html files both only contain <template><router-outlet></router-outlet></template>.
I have app.ts:
import { Router, RouterConfiguration } from 'aurelia-router';
export class App {
configureRouter(config: RouterConfiguration, router: Router) {
config.options.pushState = true;
config.map([
{
moduleId: './public-site/lectures',
name: 'lectures',
nav: true,
route: ['', 'lectures/:id?'],
title: 'Lectures'
}
]);
}
}
lecture/index.ts
import { Router, RouterConfiguration } from 'aurelia-router';
export class Index {
configureRouter(config: RouterConfiguration, router: Router) {
config.options.pushState = true;
config.map([
{ route: '', moduleId: './list' },
{ route: ':id', moduleId: './details' }
]);
}
}
and then I have lectures/details.ts
import { NavigationInstruction, RouteConfig, RoutableComponentActivate } from 'aurelia-router';
export class LectureDetails implements RoutableComponentActivate {
activate(params: any, routeConfig: RouteConfig, navigationInstruction: NavigationInstruction): Promise<any> {
debugger;
}
}
and lecture\list.html
<template>
<div repeat.for="lecture of lectures" class="grid-body-cell" click.delegate="navigateToLecture(lecture)">
${lecture.title}
</div>
</template>
lecture\list.ts
import { autoinject } from 'aurelia-framework';
import { Router } from 'aurelia-router';
#autoinject()
export class LecturesList {
constructor(private router: Router) {}
navigateToLecture(lecture: {id:number}) {
this.router.navigate(`#/lectures/${lecture.id}`);
}
}
When the app loads, it correctly navigates and displays the list page, but when I click on any of the lectures in the grid, the url updates to /lectures/1, but my debugger statement never gets hit. I can't figure out what I'm doing wrong.
What seems to be happening is that, while the url gets updated, the router is still directing the application to the list component. Is there a way to get the router to honor and pass on the parameter to the child router?
How do I need to update my setup to get the child router to work with the parameter?
Firstly, the router element for displaying routed views is called <router-view> so your views for child routers should be: <template><router-view></router-view></template> - I believe <router-outlet> is what you use in Angular 2+ applications for routing. I am making the assumption here you are already doing that if you're seeing things being rendered.
Secondly, you have config.options.pushState = true defined on your root router configuration and then inside of your navigateToLecture method you are passing in a hash (which is what you would do if you're not using pushState). So Aurelia is removing the hash from the URL (as intended) because you're using pushState and pushState URL's don't need to use the # hack.
Thirdly, I would name your routes (and you'll discover why in a moment). Naming your routes allows you to reference them by name and use either navigateToRoute('routename', {paramsobject}) or route-href (which we discuss below).
So, in lecture/index.ts, put a name property on your routes:
import { Router, RouterConfiguration } from 'aurelia-router';
export class Index {
configureRouter(config: RouterConfiguration, router: Router) {
config.options.pushState = true;
config.map([
{ route: '', name: 'lecture-list', moduleId: './list' },
{ route: ':id', name: 'lecture-detail', moduleId: './details' }
]);
}
}
And lastly, instead of having a click event in your view which is calling router.navigate, you can use the route-href attribute which will allow you to make links work with the router. So, something like the following:
<template>
<div repeat.for="lecture of lectures" class="grid-body-cell">
<a route-href="route: lecture-detail; params.bind: { id: lecture.id }">${lecture.title}</a>
</div>
</template>
Notice how we are referencing our newly named route by its name, lecture-detail? Now Aurelia will come through and parse our link and update the href property to go where it needs too.
Hope that helps.
app.html
<template><router-view></router-view></template>
app.ts
import { Router, RouterConfiguration } from 'aurelia-router';
export class App {
configureRouter(config: RouterConfiguration, router: Router) {
config.pushState = true;
config.map([
{
moduleId: './lectures/index',
name: 'lectures',
nav: true,
route: ['', 'lectures/*id'],
title: 'Lectures'
}
]);
};
}
lectures/index.html
<template><router-view></router-view></template>
lectures/list.html
<template>
<div repeat.for="lecture of lectures" class="grid-body-cell" click.delegate="navigateToLecture(lecture)">
${lecture.title}
</div>
</template>
lectures/list.ts
import { autoinject } from 'aurelia-framework';
import { Router } from 'aurelia-router';
#autoinject()
export class LecturesList {
constructor(private router: Router) {}
navigateToLecture(lecture: {id:number}) {
this.router.navigateToRoute(`lectures`, { id: lecture.id });
}
}
This took me hours of banging my head against the wall and I'm not even sure this will continue to work when I add more routes to the lectures/index.ts, but it works for now and allows me to continue on.

Aurelia: How to access data stored in a child-router's view-model

I am fairly new to Aurelia and I am trying to understand what’s the best way to access and display data in a subpage of a child-router. The data is stored in the activate method of the child-router’s view-model. My Problem is to display the data when I first enter or reload a subpage of the child-router. Unfortunately it's not working. As soon as I have displayed a subpage and go to another, it all works fine.
child-router.js
export class ChildRouter {
configureRouter(config, router) {
config.title = 'Child Router Title';
config.map([
{ route: '', redirect: 'basics'},
{ route: ['basics', ''], name: 'basics', moduleId: './basics/basics', nav: true, title: 'Basics' },
{ route: 'data', name: 'data', moduleId: './data/data', nav: true, title: 'Data' }
]);
this.router = router;
}
activate() {
this.var1 = "Var1. Only works when I reenter a subpage.";
this.var2 = "Var2. Only works when I reenter a subpage.";
}
}
child-router.html
<template>
<require from="../components/detail-navigation.html"></require>
<h2>${router.title}</h2>
<detail-navigation router.bind="router"></detail-navigation>
<div class="page-host">
<router-view></router-view>
</div>
</template>
basics.html
<template>
<h2>Basics Title</h2>
<h3>${var1}</h3>
</template>
data.html
<template>
<h2>Data Title</h2>
<h3>${var2}</h3>
</template>
I hope you understand my problem.
Here is a link to a test projekt on git.
I am looking forward for any recommendations.
Personally, I would really recommend you not try to do this. This is introducing tight coupling between the ChildRouter page and any pages displayed as routes on it. If you need these pages to talk to each other, consider using the Dependency Injection provider to inject an instance of the same class in to each page and sharing information that way.