Using onResetPasswordLink, onEnrollmentLink, and onEmailVerificationLink methods properly in Meteor
Asked Answered
D

4

10

I was wondering if someone would be kind enough to provide a meteorpad or code example of using one of the methods listed above properly in Meteor (with iron:router). I'm struggling to understand how exactly these methods interact with my app, and it seems these methods are new enough that there isn't much good documentation on how to use them correctly. Thanks!

http://docs.meteor.com/#/full/Accounts-onResetPasswordLink

Dannadannel answered 12/1, 2015 at 17:44 Comment(0)
D
15

Ok, so I am going to post what I ended up learning and doing here so others can use it as a reference. I'll do my best to explain what is happening as well.

As can be seen in the other comments, the 'done' function passed to the Accounts.on****Link callback was the main part that tripped me up. This function only does one thing - re-enables autoLogin. It's worth noting that the 'done' function/autoLogin is a part of one of the core 'accounts' packages, and cannot be modified. 'autoLogin' is used in one particular situation: User A tries to reset his or her pw on a computer where User B is currently logged in. If User A exits the reset password flow before submitting a new password, then User B will remain logged in. If User A completes the reset password flow, then User B is logged out and User A is logged in.

The pattern used to handle 'done' in the accounts-ui package, and what I ended up doing, assigns 'done' to a variable that can then be passed to your template event handler function, and run once your reset password logic is complete. This variable assignment needs to be done in the Accounts.on****Link callback, but the callback can be placed in any top-level client side code (just make sure you assign the scope of the variables correctly). I just put it at the start of my reset_password_template.js file (I've only done this for resetting passwords so far, but the pattern should be similar):

client/reset_password_template.js:

// set done as a variable to pass
var doneCallback;

Accounts.onResetPasswordLink(function(token, done) {
  Session.set('resetPasswordToken', token);  // pull token and place in a session variable, so it can be accessed later 
  doneCallback = done;  // Assigning to variable
});

The other challenge of using these on****Link callbacks is understanding how your app 'knows' the callback has been fired, and what needs to be done by the app. Since iron:router is so tightly integrated with Meteor, it's easy to forget it is a separate package. It's important to keep in mind these callbacks were written to operate independently of iron:router. This means when the link sent to your email is clicked, your app is loaded at the root level ('/').

***Side note - There are some other answers here on StackOverflow that offer ways to integrate with iron:router, and load a specific route for each link. The problem for me with these patterns was that they seemed a bit hackish, and not in line with the 'meteor' way. More importantly, if the core Meteor team decides to alter the path of these registration links, these routes would break. I tried calling Router.go('path'); in the on****Link callback, but for some reason this didn't work in Chrome and Safari. I would love to have a way to handle specific routes for each of these emailed links, thus eliminating the need for constantly setting and clearing Session variables, but I couldn't think of a good solution that worked.

Anyways, as @stubailo described in his answer, your app is loaded (at the root level), and the callback is fired. Once the callback is fired, you have your session variable set. You can use this session variable to load the appropriate templates at the root level using the following pattern:

client/home.html (or your landing page template)

{{#unless resetPasswordToken}}
  {{> home_template}}
{{else}}
  {{> reset_password_template}}
{{/unless}}

With this, there are few things you need to take care of in your reset_password_template.js file, and home.js:

client/home.js

// checks if the 'resetPasswordToken' session variable is set and returns helper to home template
Template.home.helpers({
  resetPasswordToken: function() {
    return Session.get('resetPasswordToken');
  }
});

client/reset_password_template.js

// if you have links in your template that navigate to other parts of your app, you need to reset your session variable before navigating away, you also need to call the doneCallback to re-enable autoLogin
Template.reset_password_template.rendered = function() {
  var sessionReset = function() {
    Session.set('resetPasswordToken', '');
    if (doneCallback) {
      doneCallback();
    }    
  }

  $("#link-1").click(function() {
    sessionReset();
  });

  $('#link2').click(function() {
    sessionReset();
  });
}

Template.reset_password_template.events({
  'submit #reset-password-form': function(e) {
    e.preventDefault();

    var new_password = $(e.target).find('#new-password').val(), confirm_password = $(e.target).find('#confirm-password').val();

    // Validate passwords
    if (isNotEmpty(new_password) && areValidPasswords(new_password, confirm_password)) {
      Accounts.resetPassword(Session.get('resetPasswordToken'), new_password, function(error) {
        if (error) {
          if (error.message === 'Token expired [403]') {
            Session.set('alert', 'Sorry, this link has expired.');
          } else {
            Session.set('alert', 'Sorry, there was a problem resetting your password.');          
          }
        } else {
          Session.set('alert', 'Your password has been changed.');  // This doesn't show. Display on next page
          Session.set('resetPasswordToken', '');
          // Call done before navigating away from here
          if (doneCallback) {
            doneCallback();
          }
          Router.go('web-app');
        }
      });
    }

    return false;
  }
});

Hopefully this is helpful for others who are trying to build their own custom auth forms. The packages mentioned in the other answers are great for many cases, but sometimes you need additional customization that isn't available via a package.

Dannadannel answered 13/1, 2015 at 13:54 Comment(2)
Wow, great answer! Do you have any thoughts on how this could be documented better? The docs are hard to reason about because there are so many different Accounts methods and it might not be clear how to use them together.Mews
Yeah, I completely understand your concern with the docs and the different methods. The two main things I would clarify are 1) how exactly the done function works, and if it needs to be passed to an Template.myTemplate.events handler (maybe provide a code example), and 2) that when the links are clicked, the app loads at the root level.Dannadannel
M
7

I wrote this method, so hopefully I can give a good example of how to use it.

It's meant to be in conjunction with Accounts.sendResetPasswordEmail and Accounts.resetPassword (http://docs.meteor.com/#/full/accounts_sendresetpasswordemail and http://docs.meteor.com/#/full/accounts_resetpassword).

Basically, let's say you want to implement your own accounts UI system instead of using the accounts-ui package or similar. If you want to have a password reset system, you need three things:

  1. A way to send an email with a password reset link
  2. A way to know when the user has clicked the reset link
  3. A method to actually reset the password

Here is how the flow should work:

  1. The user clicks a link on your page that says "Reset password"
  2. You find out which user that is (possibly by having them enter their email address), and call Accounts.sendResetPasswordEmail
  3. The user clicks the reset password link in the email they just received
  4. Your app is loaded and registers a callback with Accounts.onResetPasswordLink
  5. The callback is called because the URL has a special fragment in it with the password reset token
  6. This callback can display a special UI element that asks the user to input their new password
  7. The app calls Accounts.resetPassword with the token and the new password
  8. Now the user is logged in and they have a new password

This is a little complicated because it is the most advanced and custom flow possible. If you don't want to mess around with all of these callbacks and methods, I would recommend using one of the existing accounts UI packages, for example accounts-ui or https://atmospherejs.com/ian/accounts-ui-bootstrap-3

For some example code, take a look at the code for the accounts-ui package: https://github.com/meteor/meteor/blob/devel/packages/accounts-ui-unstyled/login_buttons_dialogs.js

Mews answered 12/1, 2015 at 18:59 Comment(2)
So I looked through the accounts-ui code, and it helped. I appreciate you linking to that. I was struggling with the 'done' function being passed to the callback. It looks like this just re-enables autologin? I'm not sure exactly what this does, but does it need to be called from your UI flow (after submitting your password change form and running Accounts.resetPassword)? I want to redirect my app to a new route, but if I do so, calling 'done' later seems to be increasingly difficult.Dannadannel
"Autologin" is a process that happens every time you open a meteor app in a new tab - it looks in local storage for an existing login. For the reset password flow, we don't want to use the existing login because then you might end up in a situation where you are resetting the password for account A while logged into account B, which could be confusing. The done callback is important in case the user wants to cancel the password reset flow, and you want to continue with the normal autologin process.Mews
H
1

Per the documentation:

You can construct your own user interface using the functions below, or use the accounts-ui package to include a turn-key user interface for password-based sign-in.

Therefore, those callback are for rolling your own custom solution. However, I would recommend using one of the following packages below, with accounts-entry being my preferred solution:

Hylan answered 12/1, 2015 at 18:41 Comment(3)
Thanks Chip. I'm actually trying to roll my own custom solution in our app. It's all working with the exception of these callbacks. I'd like to understand how these work because the way I'm doing it now seems a bit 'hackish'. I'm under the impression these callbacks will help clean up that code. If I figure it out, I'll try to post here for everyone else.Dannadannel
@Dannadannel - Ok, sounds good. Are the emails being sent out? If not, I would be sure to get that working first. After that I would add the callbacks inside a client-side file as per the docs and then add console.log statements to see when they fire. The callback docs are definitely light on examples IMHO, but the basic descriptions make sense, so I think the key is getting the emails sent to further troubleshoot.Hylan
So yeah the emails were being sent. I clicked on the links, and everything seemed to work, but I was confused about the 'done' callback and how it operated. Looking through the code @Mews provided above helped a lot. From what I understand, 'done' just allows the app to move forward with logging in. I was hoping I could run a redirect through 'done', or do some other action.Dannadannel
B
0

It's been a year since this question but I just came up with the same problem. Following your solution, what I found is that you could use the Session variable within the router and the onAfterAction hook to achieve the same, but using routes:

Router.route('/', {
  name: 'homepage',
  action: function() {
    if (Session.get('resetPasswordToken')) {
      this.redirect('resetPassword', {token: Session.get('resetPasswordToken')});
    } else {
      this.render('home');
    }
  }
});

Router.route('/password/reset/:token', {
  name: 'resetPassword',
  action: function () {
    this.render('resetPassword');
  },
  data: function() {
    return {token: this.params.token};
  },
  onAfterAction: function () {
    Session.set('resetPasswordToken', '');
  }
});

Of course, you will need also:

Accounts.onResetPasswordLink(function(token, done){
  Session.set('resetPasswordToken', token);
  doneResetPassword = done;
});
Beamy answered 20/2, 2016 at 17:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.