Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Iron-router swallows Accounts.sendEnrollmentEmail #3

Closed
samhatoum opened this issue Jul 13, 2013 · 34 comments
Closed

Iron-router swallows Accounts.sendEnrollmentEmail #3

samhatoum opened this issue Jul 13, 2013 · 34 comments

Comments

@samhatoum
Copy link

I'm using a Meteor method to send an enrollment email to users, which looks like this:

http://localhost:3000/#/enroll-account/uC537KPMJ5ojPwzeF

I've verified that by moving the packages directory out of my app (to remove iron-router) the link above correctly presents the "change password" dialog, however when the router is in there, the dialog does not appear.

I'm using the accounts-ui package, which handles the password behavior.

Suggestions?

@cmather
Copy link
Contributor

cmather commented Jul 13, 2013

iron-router doesn't deal with hashbang urls yet. Does accounts-ui use the hashbang url to open the dialog?

@cmather
Copy link
Contributor

cmather commented Jul 13, 2013

As a short-term solution, you could create your own enroll account route and handle that route by manually calling the enroll account method on Meteor.

@samhatoum
Copy link
Author

So I've got this working like this:

Router file:

    Router.map(function () {
        this.route('resetPassword', {
            controller: 'AccountController',
            path: '/reset-password/:token',
            action: 'resetPassword'
        });
        this.route('verifyEmail', {
            controller: 'AccountController',
            path: '/verify-email/:token',
            action: 'verifyEmail'
        });
        this.route('enrollAccount', {
            controller: 'AccountController',
            path: '/enroll-account/:token',
            action: 'resetPassword'
        });
    });

    AccountController = RouteController.extends({
        resetPassword: function () {
            // NOTE: prompt below is very crude, but demonstrates the solution
            Accounts.resetPassword(this.params.token, prompt('enter new password'), function () {
                Router.go('/');
            });
        },
        verifyEmail: function () {
            Accounts.verifyEmail(this.params.token, function () {
                Router.go('/');
            });
        }
    });

And a server file that overrides the urls with # paths that Meteor creates, so that the Iron-Router can work:

(function () {
    "use strict";

    Accounts.urls.resetPassword = function (token) {
        return Meteor.absoluteUrl('reset-password/' + token);
    };

    Accounts.urls.verifyEmail = function (token) {
        return Meteor.absoluteUrl('verify-email/' + token);
    };

    Accounts.urls.enrollAccount = function (token) {
        return Meteor.absoluteUrl('enroll-account/' + token);
    };

})();

@cmather
Copy link
Contributor

cmather commented Jul 26, 2013

@samhatoum, Awesome. Glad you got this working in the short-term. Was this an issue with support a hash url? Curious because it will impact how we end up supporting IE<10.

@samhatoum
Copy link
Author

Yep it was. If you look at the Accounts.urls.* methods in the last code snip, that's where I've forced Meteor to create paths without #'s when constructing links for the mail shots.

@cmather
Copy link
Contributor

cmather commented Aug 23, 2013

Hey @samhatoum, See my comment on the reference above. I think this issue has been fixed now. Instead of wiping out the html body (along with the accounts-ui dialogs) now the router appends to the body.

@dgtlife
Copy link

dgtlife commented Sep 7, 2013

I'm on 0.5.4 and cannot grab the token from a /#/reset-password/token URL. If I change the URL to /reset-password/token I can grab it just fine. Is the reference to a fix above referring only to the append to body, and not to the /#/ issue? ... and I need to apply a workaround like Sam's? Please confirm.

@wbashir
Copy link

wbashir commented Sep 7, 2013

@dgtlife do you by any chance have the HTML5-History package or the 'page-ie-support' package in your .packages file ? If so, i had to remove those packages as they were removing the '/#/' from the URL

@wbashir
Copy link

wbashir commented Sep 7, 2013

Ignore my last comment, i was confused about the rendering versus supporting of '/#/' urls which i believe is not supported yet @cmather ?

@cmather
Copy link
Contributor

cmather commented Sep 7, 2013

Hey @dgtlife, I thought this was working for me a few weeks ago when I tried it out. I'll take a closer look after this next round of improvements on the rendering branch. I'll make sure to test this case.

@dgtlife
Copy link

dgtlife commented Sep 7, 2013

Thanks @cmather. As background, here is my list of packages:

standard-app-packages
preserve-inputs
accounts-base
accounts-facebook
accounts-google
accounts-twitter
accounts-password
bootstrap
amplify
iron-router
email

I've currently deployed an override of the Meteor URLs to remove "#/" and everything works fine.

@vincenzoauteri
Copy link

Hi, just wanted to let you know that other users keep having this issue.

http://stackoverflow.com/questions/19112450/meteor-account-email-verify-fails-two-ways/

@cmather
Copy link
Contributor

cmather commented Oct 4, 2013

Hey guys I'm pretty sure this is fixed on dev. It just hasn't been released yet. If it still isn't working for you on the dev branch let me know. Actually, let me know either way :-).

PS - Thanks for the link to SO. I had missed that.

@cmather
Copy link
Contributor

cmather commented Oct 4, 2013

To be more clear, to test this scenario I did the following:

  1. meteor add iron-router
  2. meteor add accounts-password
  3. meteor add accounts-ui

I pasted this url: 'http://localhost:3000/#/enroll-account/uC537KPMJ5ojPwzeF' into the browser. The accounts-base package properly sees the hash, and sets the proper Accounts reactive variable which causes the dialog box to open.

@dgtlife
Copy link

dgtlife commented Oct 4, 2013

Hmmm... I don't use accounts-ui, but I do use accounts-base, and accounts-password. Here's what's in my packages file:

standard-app-packages
preserve-inputs
accounts-base
accounts-facebook
accounts-google
accounts-twitter
accounts-password
amplify
email
http
spiderable
underscore
jquery-validation
moment
bootstrap-3
iron-router

My tests basically involve console logging the token in the Route Controller function to see if it is properly parsed out. I tried with dev (4818286), inherit-handles (4129706), and initial-state (9418fbd). For me, dev is completely broken; it does not render anything but

<body>
[object DocumentFragment]
</body>

With the other two, both fail to parse out the token with '/#/enroll-account/...', but pass with '/enroll-account/...'.

This is how I have had the routes and controller that I use with the overridden Accounts.urls.enrollAccount function:

this.route('enrollAccount', {
    path: '/enroll-account/:token',
    controller: 'SetPasswordController'
  });

...

this.route('setPassword', {
    path: '/set-password/:token',
    controller: 'SetPasswordController'
  });

...

SetPasswordController = RouteController.extend({
  run: function () {
    var setPasswordToken = this.params.token;
    Session.set('setPasswordToken', setPasswordToken);
    console.log(setPasswordToken);
    this.render('setPassword');
  }
});

The above works fine with master (0.5.4) and the other commits, but not with dev, since dev appears to be currently hosed. By "works" I mean that if I use (paste in and hit Enter) a tokenized url without the '/#' as in '/enroll-account/uC537KPMJ5ojPwzeF'), I get directed to the Set Password page and the token in correctly logged in the console. I had tested tons of user signups and password resets with this configuration.

However, when I use a tokenized url with '/#' as in '/#/enroll-account/uC537KPMJ5ojPwzeF' with the code below, I just get directed to the home page and nothing is logged in the console. Release 0.5.4 and the two commits mentioned all fail this test. And again, current dev is hosed.

this.route('enrollAccount', {
    path: '/#/enroll-account/:token',
    controller: 'SetPasswordController'
  });

...

this.route('setPassword', {
    path: '/#/set-password/:token',
    controller: 'SetPasswordController'
  });

...

SetPasswordController = RouteController.extend({
  run: function () {
    var setPasswordToken = this.params.token;
    Session.set('setPasswordToken', setPasswordToken);
    console.log(setPasswordToken);    // debug
    this.render('setPassword');
  }
});

So I'll continue to use the override as I've been doing, and perhaps take a look at the iron-router code in some spare moments (hah!).

@cmather
Copy link
Contributor

cmather commented Oct 4, 2013

I just pushed a fix for the manual rendering issue ([document fragment] in body). See #148.

For the accounts-base (sorry I had said accounts-ui previously but you're right; all the url stuff is in accounts-base) I'm observing normal Meteor behavior, but in iron-router 0.5.4 we weren't dealing with hashes properly. Now we are. Here's what I can observe is happening when I paste the enroll-account url into the browser:

  • meteor/packages/accounts-base/url_client.js: parses the window.location.hash and if it finds the 'enroll-account' hash it sets autoLoginEnabled = false, Accounts._enrollAccountToken = match[1] and then window.location.hash = ''. NOTE: accounts-base is removing the hash by setting window.location.hash to an empty string.
  • iron-router: Hashes and query strings aren't used to match routes. So the router sees this route as the root route ('/'). It will dispatch to that route accordingly. It does not do any kind of redirect. However, the hash value will be gone because Meteor stripped it. It is however available in the Accounts namespace in the following private properties:
    • Accounts._resetPasswordToken
    • Accounts._verifyEmailToken
    • Accounts._enrollAccountToken

@wbashir
Copy link

wbashir commented Oct 4, 2013

@cmather so when defining the route, is it best to add or remove the '/#' ?

@cmather
Copy link
Contributor

cmather commented Oct 4, 2013

For the accounts urls I think it's best not try conflict with Meteor internals. So, anytime Meteor sees a url that looks like /#/enroll-account or any of the other email urls, it will strip everything after the hash out of the url. This will happen as soon as the accounts-base code executes on the client.

In general, the hash fragment of a url is considered variable to the router and shouldn't be used as a dynamic segment in the path. The value of the hash fragment will be made available in the parameters object under the hash property.

Examples:

Router.map(function () {
  this.route('someRoute', { 
    path: '/somepath/:_id',
    action: function () {
      var hashValue = this.params.hash;
      var _idValue = this.params._id;
    }
  });
});

For the above code both of the below urls will dispatch to the someRoute route:

  • /somepath/1/#someHashValue
  • /somepath/2/#someOtherHashValue

EDIT
NOTE: The value of the hash may not be available in your route in the case of accounts urls if Meteor code has stripped the value before the route has been dispatched. Currently there's no guarantee that a route will be dispatched before the Meteor accounts-base code executes.

@dgtlife
Copy link

dgtlife commented Oct 5, 2013

First of all, dev has improved, but is still broken; I put the comment with Issue #148.

OK, I now understand what has been going on with those match calls in meteor/packages/accounts-base/url_client.js looking for hash fragments and stripping out the fragment. I also read the why cited for this.

// reset password urls use hash fragments instead of url paths/query
// strings so that the reset password token is not sent over the wire
// on the http request

To circumvent all of this, I can override Accounts.sendEnrollmentEmail and Accounts.sendResetPasswordEmail functions to send the raw token, and the url to my Set Password page, in the email. Once there, the user can copy and paste the token in to a form field.

Another option that works is to override your Accounts.urls by adding your desired destination path before the hash, so that when Meteor strips everything out you end up at your desired location and your Accounts._resetPasswordToken is still intact.

Accounts.urls.resetPassword = function (token) {
    return Meteor.absoluteUrl('set-password/#/reset-password/' + token);
  };

@DenisGorbachev
Copy link

+1

@patrickocoffeyo
Copy link

Interesting.... The latest dev version did not work. I ended up adding my own route, and handling the Account._* variables myself.

@micahalcorn
Copy link

@samhatoum's solution seems to still be necessary. Although I had to change the method call on RouteController from .extends to .extend.

@wbashir
Copy link

wbashir commented May 22, 2014

Any updates to this

@KrishManohar
Copy link

I am using @samhatoum's solution, and still not working.

Its getting redirected to my index page "/"

@dgtlife
Copy link

dgtlife commented May 30, 2014

@KrishManohar If you could post a summary of what you did in code, perhaps I can help. Otherwise, it's impossible to know what exactly you did "using samhatoum's solution".

@KrishManohar
Copy link

I posted more information here: https://groups.google.com/forum/#!topic/meteor-talk/p4xlSiaQKq8

So my application signup sends a verification email to our users:

http://localhost:3000/#/verify-email/11da222dadtokendadadtTOKEN_Dad

After the user gets the email and click on it - it ends up

Redirecting to our home page.

I follow #3 = samhatoum post

Router Code:

  this.route('verifyEmail',
    {
      controller: 'AccountController',
      path: '/verify-email/:token',
      action: 'verifyEmail'
    });

Controller Code:

AccountController = RouteController.extend({
  verifyEmail: function () {
    Accounts.verifyEmail(this.params.token, function () {
      Router.go('/login');
    });
  }
}); 

My Before Action Code:

var mustBeSignedIn = function(pause) {
  if (!(Meteor.user() || Meteor.loggingIn())) {
    Router.go('login');
    pause();
  }
};

var goToDashboard = function(pause) {
  if (Meteor.user()) {
    Meteor.logout();
    Router.go('library');
    pause();
  }
};
Router.onBeforeAction(goToDashboard, {only:['home', 'login'] }); //Add Back Login
Router.onBeforeAction(mustBeSignedIn, {except: ['login', 'verifyEmail', 'resetPassword']}); 

Now Server Code For Absolute URL:

(function () {
  "use strict";
  Accounts.urls.resetPassword = function (token) {
    return Meteor.absoluteUrl('reset-password/' + token);
  };
  Accounts.urls.verifyEmail = function (token) {
    return Meteor.absoluteUrl('verify-email/' + token);
  };
})();

@copleykj
Copy link

Iron Router is not what is causing this. The meteor accounts system detects
the token in the URL and strips it out and stores it in a variable under
the Accounts namespace.

@dgtlife
Copy link

dgtlife commented May 30, 2014

@KrishManohar I don't think you are following samhatoum's solution properly. If you were, the URL generated by the server that's placed in the email would not have a "#" in it.

Meteor's accounts-base package contains code like this

match = window.location.hash.match(/^\#\/verify-email\/(.*)$/);
if (match) {
  autoLoginEnabled = false;
  Accounts._verifyEmailToken = match[1];
  window.location.hash = '';
}

This code strips out everything after the "#", before iron-router can get to parse it, and it leaves your path as "/". So, where are you placing the server code which is supposed to generate a URL without a "#"?

@KrishManohar
Copy link

So its my fault...

That email was generated before i looked up why... and use samhatoum's code.

I will run another signup process....

Update: It work! Thank you so much for pointing this out... I should have read meteor doc on absolute url

@dgtlife
Copy link

dgtlife commented May 30, 2014

Great! Good luck with the rest of your app.

@KrishManohar
Copy link

Thank you.

@matteosaporiti
Copy link

Just a quick question, is there a possibility that in the near future the issue is fixed by a code change either in iron-router or in meteor?
I'm asking the question because I found this issue when testing e-mail validation, and if there is a possibility of it being fixed before the end of the month I'd prefer not to apply a fix I will remove later.
If that is unlikely I will apply @samhatoum solution I guess.

@dgtlife
Copy link

dgtlife commented Jun 9, 2014

This is really a question to pose to the authors/contributors of Iron Router and Meteor. I believe any change to the behavior you observed is unlikely (look at the dates on this thread). I have worked around it and moved on.

@exocode
Copy link

exocode commented Mar 7, 2015

Sorry,

Can someone tell me please when Accounts._resetPasswordToken get resetted?

After the password is successully confirmed I get redirected to my UserDashboard Page. But then I logout and should be redirected to login but I get redirected to the password confirmation again because Accounts._resetPasswordToken is still set.

#Router
Router.route('/register', {
  name: 'register',
  template: 'Register',
  title: 'Register'
})

Router.route('/', {
  name: 'login',
  layoutTemplate: 'Frontpage',
  layout: 'Login',
  title: 'Home'
});
@LoginController = RouteController.extend(
  onBeforeAction: ->
    if Meteor.user()
      Router.go('dashboard')
    else
      @next()

  waitOn: ->

  data: ->

  action: ->
    # in order to catch the iron:router hashtag issue
    # requests will be redirected here
    #
    console.log "from action:"
    console.log Accounts._resetPasswordToken
    if Accounts._resetPasswordToken
      Router.go('recover_password', token: Accounts._resetPasswordToken)
    else
      @render()
)
@RecoverPasswordController = RouteController.extend(
  onBeforeAction: ->
    if Meteor.user()
      Router.go('dashboard')
    else
      @next()


  waitOn: ->

  data: ->

  action: ->
    @render()

)
# RecoverPassword: Event Handlers and Helpers

Template.RecoverPassword.events
  'submit form#form-lock': (e, tpl) ->
    e.preventDefault
    if AutoForm.validateForm('form-lock')
      email = tpl.find('#recover-email-field').value
      Session.set('loading', true);
      Accounts.forgotPassword(email: email, (err) ->
        if (err)
          Session.set('errorMessages', err.reason)
        else
          Session.set('errorMessages', 'Email Sent! Please check your email.')
      )
      Session.set('loading', false);
    else
      Session.set 'errorMessages', 'Form not valid'
    false

  'submit form#form-new-password': (e, tpl) ->
    e.preventDefault

    if AutoForm.validateForm('form-new-password')
      password = tpl.find('#new-password-field').value
      token = Session.get('resetPassword')
      Accounts.resetPassword(token, password, (err) ->
        if err != undefined
          console.log "ERRROR"
          Session.set('errorMessages', err.reason)
        else
          console.log 'SUCCESS'
          Session.set('errorMessages', null)
          Session.set('loading', true)
          Session.set('resetPassword', null)
          Router.go('login')
      )
    else
      Session.set 'errorMessages', 'Form incorrect. Please review it.'
    false

Template.RecoverPassword.helpers

  recoverPasswordSchema: ->
    Schema.UserRecoverPassword

  newPasswordSchema: ->
    Schema.UserNewPassword

  errorMessages: ->
    Session.get('errorMessages')

  resetPassword: ->
    Session.get('resetPassword')


# RecoverPassword: Lifecycle Hooks
Template.RecoverPassword.created = ->

Template.RecoverPassword.rendered = ->

Template.RecoverPassword.destroyed = ->


if (Accounts._resetPasswordToken)
  Session.set('resetPassword', Accounts._resetPasswordToken)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests