Redirecting not logged-in users with iron-router... Again
Asked Answered
N

5

20

I am fighting with the common need for redirecting a user to a login page if he is not logged-in (Meteor v0.8.0 on Windows 7).

There are several similar questions on stackoverflow, but no answer seems to work for me.

Won't work #1: render()

From the documentation:

onBeforeAction: function () {
  if (!Meteor.user()) {
    // render the login template but keep the url in the browser the same
    this.render('login');

    // stop the rest of the before hooks and the action function 
    this.stop();
  }
},

Two issues here:

1- The documentation is outdated. There is no this.stop() function anymore. As stated here, the code should be :

onBeforeAction: function (pause) {
  if (!Meteor.user()) {
    // render the login template but keep the url in the browser the same
    this.render('login');

    // stop the rest of the before hooks and the action function 
    pause();
  }
},

2- This works only if the route has no layoutTemplate. If it has one, the login template is rendered in the {{>yield}} of the layoutTemplate. This is usually not what you want for a login page.

Won't work #2: Router.go() or this.redirect()

Defining a route for the login page sounds like the natural way. You can then do:

Router.onBeforeAction(function(pause) {
    if (!Meteor.user()) {
        pause();
        Router.go('\login');
    }
}, {except: ['login']});

Or:

Router.onBeforeAction(function() {
    if (!Meteor.user())
        this.redirect('\login');
}, {except: ['login']});

But strangely, there is still an issue if the original route (before redirect) has a layoutTemplate: the /login template is rendered inside the {{yield}}. Which again is not what you usually want (and definitely not what you expect, as the /login template has no layoutTemplate defined).

I found a way to partially solve this:

Router.onBeforeAction(function() {
    if (!Meteor.user()) {
        var that = this;
        setTimeout(function() { that.redirect('\login'); }, 0);
    }
}, {except: ['login']});

Now everything is fine: the /login template renders as a clean page... Except that the layoutTemplate of the original route briefly blinks before the /login template is displayed.

Have you got this same problem?

Naturalist answered 2/5, 2014 at 13:38 Comment(0)
M
9

Ok, so it seems that the render function on a route only renders a template into the current layout. To render a template into a different layout you have to call this.setLayout('templateName'). The one caveat seems to be that you'll need to set the layout back after login.

onBeforeAction: function(pause) {
    var routeName = this.route.name;

    if (_.include(['login'], routeName))
        return;

    if (! Meteor.userId()) {
        this.setLayout("newLayout");
        this.render('login');

        //if you have named yields it the login form
        this.render('loginForm', {to:"formRegion"});

        //and finally call the pause() to prevent further actions from running
        pause();
    }else{
        this.setLayout(this.lookupLayoutTemplate());
    }
}

You could also just render the login template as the layout if your login template is all you need by calling this.setLayout('login')

Mediocre answered 5/5, 2014 at 15:56 Comment(6)
It works, thanks Kelly! All I had to do is Router.onBeforeAction(function() { if (!Meteor.userId()) this.setLayout("login"); }}, {except: ['login']}); It seems there is no need for calling pause()Naturalist
Glad to help! Also figuring this out actually help me learn something new about the way that iron-router works.Mediocre
setLayout(undefined) keeps the login layout, so I had to create a blank template that just yields, and changed your last line to this.setLayout(this.lookupLayoutTemplate() || 'blank');Deyo
Also, if you link to another excluded route, such as signup, from your login page, it does not work unless you have a layoutTemplate set in the signup route.Deyo
the API updated. quotes from the iron router docs: onRun and onBeforeAction hooks now require you to call this.next(), and no longer take a pause() argument. So the default behaviour is reversed.... AND ..... controller.setLayout() is now controller.layout(). Usually called as this.layout("fooTemplate") inside a route action.Alcoholize
Not to your particular question, but be careful of using this pattern without doing other checks for an existing user. On the client console if I were to type Meteor.userId = function() { return true; } and try again I've gotten in to this area.Conquian
S
21

You can put an if or unless template helper in the layout template.

{{#unless currentUser}}
  {{> loginPage}}
 {{else}}
  {{> yield}}
{{/unless}}
Salim answered 2/3, 2015 at 20:34 Comment(2)
Most elegant solution as it simplifies complexity when using Iron Router, no reactive rerouting if Meteor.user is updated.Donor
The issue in your solution is that if there are links to other templates/routes on the LogIn page (as sign-up, recover password, etc), they will not work because the '{{#unless currentUser}}' avoid any other route to run.Murphree
M
9

Ok, so it seems that the render function on a route only renders a template into the current layout. To render a template into a different layout you have to call this.setLayout('templateName'). The one caveat seems to be that you'll need to set the layout back after login.

onBeforeAction: function(pause) {
    var routeName = this.route.name;

    if (_.include(['login'], routeName))
        return;

    if (! Meteor.userId()) {
        this.setLayout("newLayout");
        this.render('login');

        //if you have named yields it the login form
        this.render('loginForm', {to:"formRegion"});

        //and finally call the pause() to prevent further actions from running
        pause();
    }else{
        this.setLayout(this.lookupLayoutTemplate());
    }
}

You could also just render the login template as the layout if your login template is all you need by calling this.setLayout('login')

Mediocre answered 5/5, 2014 at 15:56 Comment(6)
It works, thanks Kelly! All I had to do is Router.onBeforeAction(function() { if (!Meteor.userId()) this.setLayout("login"); }}, {except: ['login']}); It seems there is no need for calling pause()Naturalist
Glad to help! Also figuring this out actually help me learn something new about the way that iron-router works.Mediocre
setLayout(undefined) keeps the login layout, so I had to create a blank template that just yields, and changed your last line to this.setLayout(this.lookupLayoutTemplate() || 'blank');Deyo
Also, if you link to another excluded route, such as signup, from your login page, it does not work unless you have a layoutTemplate set in the signup route.Deyo
the API updated. quotes from the iron router docs: onRun and onBeforeAction hooks now require you to call this.next(), and no longer take a pause() argument. So the default behaviour is reversed.... AND ..... controller.setLayout() is now controller.layout(). Usually called as this.layout("fooTemplate") inside a route action.Alcoholize
Not to your particular question, but be careful of using this pattern without doing other checks for an existing user. On the client console if I were to type Meteor.userId = function() { return true; } and try again I've gotten in to this area.Conquian
O
3

It looks like this has something to do with waiting on subscriptions in waitOn.

The following solves the layout rendering issues for me:

Router.onBeforeAction(function() {
    if (!Meteor.user() && this.ready())
        return this.redirect('/login');
}, {except: ['login']}); 
Olmstead answered 29/8, 2014 at 19:28 Comment(1)
It should be noted that an else { this.next() } is needed.Chloe
M
0

You just need to return the the result of render() from your onBeforeAction()

onBeforeAction: function () {

  if (_.include(['login'], this.route.name)){
    return;
  }

  if (!Meteor.userId()) {

    return this.render('login');

  }
}

Also note I changed Meteor.user() to Meteor.userId(). This keeps the hook from being rerun every time the current users document chages.

Mediocre answered 2/5, 2014 at 15:47 Comment(6)
Sorry, but this does not work. When omitting the call to pause(), as you suggest, the 'login' template is never rendered. When adding pause() to what you suggest, the result is the same as in "Won't work #1" above.Naturalist
I've added a check so that the hook doesn't run for the login route.. Other than that I don't know what might cause this.. I use this code in all of my projects and it works great.Mediocre
Just to be sure, do you confirm the proposed code gets you the 'login' form to render without any layoutTemplate, even when called from a route that has one?Naturalist
I'm not sure if I follow, but generally I set a default layout template in my Router.configure().Mediocre
When you have a default layout, you probably don't care if the 'login' template is rendered inside this default layout. Instead, suppose you have 5 pages with layout1 and 5 other pages with layout2. When the user must login, you don't want the 'login' template to render inside layout1 or layout2. You want it to render on a clean page, with no template or a brand new template3. That is where the problem lies.Naturalist
Ok, I've spent the last few hours working this out the way you are using iron-router.. Finally Came up with a solution. I'll post a second answer with a proper way to achieve this.Mediocre
N
0

On Meteor 0.8.3 for me works:

Router.onBeforeAction(function () { 
  if (_.include(['formLogin'], this.route.name)){
    return;
  }

  if (!Meteor.userId()) {
    this.redirect('formLogin');
    return;
  }

});
Naumann answered 20/8, 2014 at 14:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.