Loopback should generally not need to know the URL on which it is listening.
However, there are several situations where a URL must be generated for the user to connect back. For example, User.verify and User.resetPassword. In both those cases, we need to create a link to embed in an email for the user to confirm an action by verifying their email address.
At present, both functions require host and port to be configured in config.json or related config.
Another approach is to use the URL of the request to construct a link. I did this in an afterRemote hook on User.create:
function afterRemote_create_hook(ctx, user, next) {
var url_bits, protocol, host, port
if (ctx.req.headers && ctx.req.headers.origin) {
url_bits = ctx.req.headers.origin.split(':')
protocol = url_bits[0]
host = url_bits[1].substring(2)
port = url_bits[2]
}
var app = User.app,
options = {
type: 'email',
to: user.email,
from: '[email protected]', // TODO get email sender from Settings
subject: 'Thanks for registering',
text: 'Please verify your email address!',
template: 'server/templates/verify.ejs',
redirect: '/',
protocol: protocol || 'http',
host: host || 'localhost',
port: port || 3000
};
user.verify(options, next);
So registration works fine with the inbound URL.
But I have a problem with resetPassword. I listen on "resetPasswordRequest" event but I have no context of the incoming message to determine the URL.
Is there any way at present to get the context? Or is it a feature request?
See the PR at https://github.com/strongloop/loopback/pull/337. With the implicit context propagation, you should be able to pick up the context without messing up existing method signatures.
So the context will somehow be available from an emit?
@johnsoftek Has the issue been resolved? Can I close it?
@superkhau AFAIK, no.
Context propagation relates to supplying context around a remote request. From memory, ResetPassword is quite strange in that it does not return a result for the original request. Instead the example suggests listening on an emit event. That event does not supply the original request context and therefore the original url is not available.
@bajtos @raymondfeng @ritch PTAL
Regarding the current context - see https://github.com/strongloop/loopback/issues/982. I would expect that if the event is emitted from a HTTP handler, then the event listeners should be able to access the current context even now, before #982 is fixed.
As for passing the request context to "resetPasswordRequest", perhaps that can be added to loopback. I am not familiar with the current implementation of "reset pasword", @raymondfeng and/or @ritch now more about that. If they agree it's ok to pass more data to "resetPasswordRequest" event, then a new issue should be opened for that.
You should use app.get('url') to get the canonical URL at which the application can be accessed. I am not sure if you approach based on the Origin header will work when the website making the XHR request is on a different domain than LoopBack's REST API, or when the request is sent from a native iOS/Android app.
+1
This still appears to be an issue as I am faced with an identical situation.
Attempting app.get('url') returns a URL identical to the one contained in config.json however I need to use a different URL to provide to the user after a "resetPasswordRequest" has been emitted. That URL will change across environments and over time as we have not yet moved to production.
Accessing context from the custom method that handles the user's request and is responsible for emitting the "resetPasswordRequest" event is simple enough. But the context is not accessible from the event listener User.on('resetPasswordRequest', function(info)), the listener responsible for acting on the event and dispatching the email.
I have followed the example and have a working process with the caveat that I've hard coded the URL to match my local instance. Not to revive an old topic, but I'm very curious as to any progress or workarounds that may exist.
Attempting app.get('url') returns a URL identical to the one contained in config.json however I need to use a different URL to provide to the user after a "resetPasswordRequest" has been emitted. That URL will change across environments and over time as we have not yet moved to production.
FWIW, the config can be customized using per-environment overrides like server/config.production.json. It should be also possible to reference environment variables inside server/config.json to make it even simpler.
Accessing context from the custom method that handles the user's request and is responsible for emitting the "resetPasswordRequest" event is simple enough. But the context is not accessible from the event listener User.on('resetPasswordRequest', function(info)), the listener responsible for acting on the event and dispatching the email.
IMO, this is a missing feature that we should address.
Right now, resetPassword is defined with the following arguments:
UserModel.remoteMethod(
'resetPassword',
{
description: 'Reset password for a user with email.',
accepts: [
{arg: 'options', type: 'object', required: true, http: {source: 'body'}},
],
http: {verb: 'post', path: '/reset'},
}
);
I am proposing to rename the first argument from options to params and then add a second options arg with http: 'optionsFromRequest'. Then we can modify the resetPasswordRequest to receive this new options argument as a second event arg.
UserModel.emit('resetPasswordRequest', {
- email: options.email,
+ email: params.email,
accessToken: accessToken,
user: user,
- options: options,
+ options: params,
+ }, options);
});
Thoughts?
@tjc0090 would you like to contribute this change yourself? I am happy to help you along the way. See http://loopback.io/doc/en/contrib/code-contrib.html to get you started.
I use:
function beforeRemote_resetPassword_hook (ctx, unused, next) {
var req = ctx && ctx.req
var body = req && req.body
if (body && body.email && body.origin) {
origins[body.email] = body.origin
}
next()
}
function on_resetPasswordRequest (info) {
// console.log(info.email) // the email of the requesting user
// console.log(info.accessToken.id) // the temp access token to allow password reset
var app = User.app,
options = {}
options.origin = origins[info.email] ||
info.protocol || (app && app.get('protocol')) || 'http'
+ '://'
+ info.host || (app && app.get('host')) || 'localhost'
+ ':'
+ info.port || (app && app.get('port')) || 3000
options.resetHref =
options.origin
+ '/auth/reset'
+ '?uid=' + info.user.id
+ '&accessToken=' + info.accessToken.id
I should probably store the origin in ctx.options...
Is there a workaround for accessing the current context on resetPasswordRequest? https://loopback.io/doc/en/lb3/Using-current-context.html doesn't show how to access the current context in such situation.
The reason I need this is for using i18n-node translation function, which is attached to both req and res properties of the context object. I'm able to localize messages on any remote methods and operation hooks, because I have access to the context object.
I don't think there is a workaround, but see my https://github.com/strongloop/loopback/issues/512#issuecomment-290660034 for a pointer on how to fix this problem. I am happy to help you along the way if you decide to contribute this fix.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Hopefully this is fixed in Loopback 4, so for now I'll just do like @johnsoftek with the following:
User.beforeRemote( 'resetPassword', function( ctx, model, next) {
ctx.req.body.origin = ctx.req.headers.origin;
next();
});
User.on('resetPasswordRequest', function(info) {
var origin = info.options.origin || app.get('url');
var url = origin + '/reset-password?access_token=' + info.accessToken.id;
var html = 'Click <a href="' + url + '">here</a> to reset your password';
User.app.models.Email.send({
to: info.email,
from: info.email,
subject: 'Password reset',
html: html
}, function(err) {
if (err) return console.log('> error sending password reset email');
console.log('> sending password reset email to:', info.email);
});
});
Most helpful comment
Hopefully this is fixed in Loopback 4, so for now I'll just do like @johnsoftek with the following: