I have a rails app that can handle many subdomains, and I have multiple live versions running with different domain names. This causes the URLs to be anywhere between
mywebsite.com
company1.mywebsite.com
company1.mytestwebsite.com
Devise has mailers that use links for resetting passwords and such. These links are sometimes incorrect and send me to the incorrect website because the host name is sometimes different from that of the the default URL from /config/environments/production.rb
:
config.action_mailer.default_url_options = { host: 'mywebsite.com' }
How do I pass request.host
from the controller to the Devise mailer?
If I had the host, I could create the links that send the user to the correct website
Just add one file to /config/initializers
and overwrite the devise controller
The file:
# config/initializers/devise_monkeypatch.rb
module Devise
module Models
module Recoverable
module ClassMethods
# extract data from attributes hash and pass it to the next method
def send_reset_password_instructions(attributes = {})
data = attributes.delete(:data).to_unsafe_h
recoverable = find_or_initialize_with_errors(reset_password_keys, attributes, :not_found)
recoverable.send_reset_password_instructions(data) if recoverable.persisted?
recoverable
end
end
# adjust so it accepts data parameter and sends it to next method
def send_reset_password_instructions(data)
token = set_reset_password_token
send_reset_password_instructions_notification(token, data)
token
end
# adjust so it accepts data parameter and sends to next method
protected def send_reset_password_instructions_notification(token, data)
send_devise_notification(:reset_password_instructions, token, data: data)
end
end
end
Mailer.class_eval do
# extract data from options and set it as instance variable
def reset_password_instructions(record, token, opts={})
@token = token
@data = opts.delete :data
devise_mail(record, :reset_password_instructions, opts)
end
end
end
Generate the controller and the views
rails g devise:controllers users -c=passwords
rails g devise:views
Edit the routes
# config/routes.rb
devise_for :users, controllers: {
passwords: 'users/passwords'
}
Edit the create action
class Users::PasswordsController < Devise::PasswordsController
def create
params[:user][:data] = { host: request.url.remove(request.path) }
super
end
end
Edit the view
<p>
<%= link_to 'Change my password',
edit_password_url(
@resource, reset_password_token: @token, host: @data[:host]
)
%>
</p>
Explanation below:
Looking at the source code, these are the methods that are used to get from controller to mailer
# the controller calls send_reset_pasword_instructions on class
Devise::PasswordsController#create
resource_class.send_reset_password_instructions(resource_params)
# class finds instance and calls send_reset_password_instructions on instance
Devise::Models::Recoverable::ClassMethods
def send_reset_password_instructions(attributes = {})
recoverable = find_or_initialize_with_errors(reset_password_keys, attributes, :not_found)
recoverable.send_reset_password_instructions if recoverable.persisted?
recoverable
end
Devise::Models::Recoverable
# instance calls send_reset_password_instructions_notification
def send_reset_password_instructions
token = set_reset_password_token
send_reset_password_instructions_notification(token)
token
end
# instance calls send_devise_notification
protected def send_reset_password_instructions_notification(token)
send_devise_notification(:reset_password_instructions, token, {})
end
Devise::Models::Authenticatable
# instance calls mailer
protected def send_devise_notification(notification, *args)
message = devise_mailer.send(notification, self, *args)
# Remove once we move to Rails 4.2+ only.
if message.respond_to?(:deliver_now)
message.deliver_now
else
message.deliver
end
end
# mailer
protected def devise_mailer
Devise.mailer
end
class Devise::Mailer
# mailer sets @token
def reset_password_instructions(record, token, opts={})
@token = token
devise_mail(record, :reset_password_instructions, opts)
end
All you need is to set another instance variable in the last method, but you have to edit the other methods to successfully pass the data.
Here is a comparison of original vs. changes needed:
Devise::PasswordsController
# original, will stay the same
def create
self.resource = resource_class.send_reset_password_instructions(resource_params)
yield resource if block_given?
if successfully_sent?(resource)
respond_with({}, location: after_sending_reset_password_instructions_path_for(resource_name))
else
respond_with(resource)
end
end
# override to add data
def create
params[:user][:data] = request.url
super
end
Devise::Models::Recoverable::ClassMethods
# original, will be overwritten
def send_reset_password_instructions(attributes = {})
recoverable = find_or_initialize_with_errors(reset_password_keys, attributes, :not_found)
recoverable.send_reset_password_instructions if recoverable.persisted?
recoverable
end
# extract data from attributes hash and pass it to the next method
def send_reset_password_instructions(attributes = {})
data = attributes.delete :data
recoverable = find_or_initialize_with_errors(reset_password_keys, attributes, :not_found)
recoverable.send_reset_password_instructions(data) if recoverable.persisted?
recoverable
end
Devise::Models::Recoverable
# original, will be overwritten
def send_reset_password_instructions
token = set_reset_password_token
send_reset_password_instructions_notification(token)
token
end
# adjust so it accepts data parameter and sends it to next method
def send_reset_password_instructions(data)
token = set_reset_password_token
send_reset_password_instructions_notification(token, data)
token
end
# original, will be overwritten
protected def send_reset_password_instructions_notification(token)
send_devise_notification(:reset_password_instructions, token, {})
end
# adjust so it accepts data parameter and sends to next method
protected def send_reset_password_instructions_notification(token, data)
send_devise_notification(:reset_password_instructions, token, data: data)
end
Devise::Models::Authenticatable
# original, stays the same
protected def send_devise_notification(notification, *args)
message = devise_mailer.send(notification, self, *args)
# Remove once we move to Rails 4.2+ only.
if message.respond_to?(:deliver_now)
message.deliver_now
else
message.deliver
end
end
# original, stays the same
protected def devise_mailer
Devise.mailer
end
class Devise::Mailer
# extract data from options and set it as instance variable
def reset_password_instructions(record, token, opts={})
@token = token
@data = opts.delete[:data]
devise_mail(record, :reset_password_instructions, opts)
end
Remove the code that remains unchanged as well as the controller code because we will change that in another file. Wrap it into a neat little module and adjust it so we add methods to classes and we pass Hash objects instead of Parameter objects.
Here is the final version
module Devise
module Models
module Recoverable
module ClassMethods
# extract data from attributes paramater object and convert it to hash
# and pass it to the next method
def send_reset_password_instructions(attributes = {})
data = attributes.delete(:data).to_unsafe_h
recoverable = find_or_initialize_with_errors(reset_password_keys, attributes, :not_found)
recoverable.send_reset_password_instructions(data) if recoverable.persisted?
recoverable
end
end
# adjust so it accepts data parameter and sends it to next method
def send_reset_password_instructions(data)
token = set_reset_password_token
send_reset_password_instructions_notification(token, data)
token
end
# adjust so it accepts data parameter and sends to next method
protected def send_reset_password_instructions_notification(token, data)
send_devise_notification(:reset_password_instructions, token, data: data)
end
end
end
Mailer.class_eval do
# extract data from options and set it as instance variable
def reset_password_instructions(record, token, opts={})
@token = token
@data = opts.delete :data
devise_mail(record, :reset_password_instructions, opts)
end
end
end
Answered this myself because I couldn't find a solution online even though there are others who also asked about this. Posted this on Stack Overflow to maximize helping others out with the same problem.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With