I found this great tutorial on Github about how to create a custom mailer from Devise. https://github.com/plataformatec/devise/wiki/How-To:-Use-custom-mailer
I implemented the code and the logic makes complete sense to me however when I deploy, I can't get the code to work when I set my config.mailer = 'MyMailer' in initialize/devise.rb. There are no errors in the terminal and it seems that everything got processed just like normal however the email never makes it to the inbox.
If I change config.mailer = 'Devise::Mailer', the email gets delivered correctly. However since MyMailer inherits from Devise::Mailer, shouldn't it work?
Is anyone else having this issue? Any ideas what I can do to troubleshoot?
Here is my "mailers/my_mailer.rb":
class MyMailer < Devise::Mailer
helper :application # gives access to all helpers defined within `application_helper`.
# helper :devise # gives access to all helpers defined within `devise_helper`.
#include Devise::Controllers # Optional. eg. `confirmation_url`
include Devise::Mailers::Helpers
def invoice_payment_failed_email(user, listing)
#user = user
#listing = listing
#url = 'http://example.com/login'
mail(to: #user.email, subject: 'Payment Failed')
end
end
Here is my "initializers/devise.rb":
Devise.setup do |config|
config.secret_key = ENV["DEVISE_SECRET_KEY"]
# Configure the class responsible to send e-mails.
config.mailer = 'MyMailer'
Here is my call to the deliver the mail:
# email customer
MyMailer.invoice_payment_failed_email(#user, #listing).deliver
*Update:
OMG... after a whole day of racking my brain trying to figure this out, it turns out that the code logic is fine but the problem was that my From: needed to match the Postmark sender accounts. This is why I wasn't getting any terminal errors. The deliver request was being sent correctly to Postmark but since my From address wasn't matching, postmark just never delivered the email!
Related
I am wondering how to limit the connection to a channel or the streaming of messages over a channel in rails5. Currently I groups with users in the groups working with pundit and the connection to the websocket happens within that group. If a malicious user randomly guessed groups they could potentially read a message over a socket they shouldn't.
When you create a new message the following code is run in my controller:
if message.save
ActionCable.server.broadcast(
"messages_{message.groupchat_id}_channel",
message: message.content,
user: message.user.email
)
head :ok
end
I have no idea what I'm doing.
Here's the solution I found to use Pundit with Activecable.
First we need access to the user model. You can do that by following the instructions in the Action Cable Overview - Connection Setup. Mainly you need to change the code in connection.rb
# app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
private
def find_verified_user
if verified_user = User.find_by(id: cookies.encrypted[:user_id])
verified_user
else
reject_unauthorized_connection
end
end
end
end
Note: you may need to use a different way of finding your user. I'm using devise_token_auth and so needed to pass the uid, token, and client_id to the connection.rb and then got the user via this code:
if user && user.valid_token?(token, client_id)
user
else
reject_unauthorized_connection
end
I mention this just because how you get your user may vary. The main thing is that you need to use identified_by current_user and set it.
Another thing which I did not immediately find in the documentation, is that the current_user is now accessible by your channels. Since the user name may be different than your pundit_user name, I found it easiest to manually pass the user to Pundit at that point. So in subscribing with my channel file, I had this code:
def subscribed
message = MessagePolicy::Scope.new(self.current_user, Project).resolve.find(params[:message])
stream_for message
end
You could of course also manually authorize this way, instead of using Scope:
MessagePolicy.new(self.current_user, message).show?
You can add another layer of security for actioncable connections in app/channels/application_cable/connection.rb file.
You can follow this tutorial. I think this give you some idea:
https://www.learnenough.com/action-cable-tutorial#sec-login_protection
I need to write a log when somebody failes to log in to my app (to track bruteforce attempts). Also I decided to log successful authentications.
So I created a SessionsController < Devise::SessionsController and tried to override the sessions#create method like that: https://gist.github.com/3884693
The first part works perfectly, but when the auth failes rails throws some kind of an exception and never reaches the if statement. So I don't know what to do.
This answer to a previous SO question - Devise: Registering log in attempts has the answer.
The create action in the devise controller calls warden.authenticate!, which attempts to authenticate the user with the supplied params. If authentication fails then authenticate! will call the devise failure app, which then runs the SessionsController#new action. Note, any filters you have for the create action will not run if authentication fails.
So the solution is to add a filter after the new action which checks the contents of env["warden.options"] and takes the appropriate action.
I tried out the suggestion, and was able to log both the successful & failed login attempts. Here is the relevant controller code:
class SessionsController < Devise::SessionsController
after_filter :log_failed_login, :only => :new
def create
super
::Rails.logger.info "\n***\nSuccessful login with email_id : #{request.filtered_parameters["user"]}\n***\n"
end
private
def log_failed_login
::Rails.logger.info "\n***\nFailed login with email_id : #{request.filtered_parameters["user"]}\n***\n" if failed_login?
end
def failed_login?
(options = env["warden.options"]) && options[:action] == "unauthenticated"
end
end
The log has the following entries:
For a successful login
Started POST "/users/sign_in"
...
...
***
Successful login with email_id : {"email"=>...
***
...
...
Completed 302 Found
For a failed login
Started POST "/users/sign_in"
...
...
Completed 401 Unauthorized
Processing by SessionsController#new as HTML
...
...
***
Failed login with email_id : {"email"=>...
***
...
...
Completed 302 Found
Prakash's answer is helpful, but it's not ideal to rely on SessionsController#new to be run as a side effect. I believe this is cleaner:
class LogAuthenticationFailure < Devise::FailureApp
def respond
if request.env.dig('warden.options', :action) == 'unauthenticated'
Rails.logger.info('...')
end
super
end
end
...
Devise.setup do |config|
config.warden do |manager|
manager.failure_app = LogAuthenticationFailure
end
Check out Graeme's answer if you'd prefer to hook into Warden's callbacks (Devise is implemented using Warden).
I had the same question but was unable to resolve it using the "warden.options" since, in my case, these were being cleared before redirecting to the sessions#new action. After looking into a few alternatives that I judged to be too brittle (because they involved extending some Devise classes and aliasing existing methods), I wound up using some callbacks provided by Warden. It works better for me because the callback is invoked inside the current request-response cycle and the parameters are all preserved in the env object.
These callbacks are named and appear to be designed to solve this and related problems. And they are documented!
Warden supports the following callbacks as of warden-1.2.3:
after_set_user
after_authentication (useful for logging successful sign ins)
after_fetch (alias for after_set_user)
before_failure (useful for logging failed sign ins - example below)
after_failed_fetch
before_logout
on_request
Each callback is set directly on the Warden::Manager class (may be inside config/initializers/devise.rb). To track a failed authentication attempt I added this:
Warden::Manager.before_failure do |env, opts|
email = env["action_dispatch.request.request_parameters"][:user] &&
env["action_dispatch.request.request_parameters"][:user][:email]
# unfortunately, the User object has been lost by the time
# we get here; so we take a db hit because I care to see
# if the email matched a user account in our system
user_exists = User.where(email: email).exists?
if opts[:message] == :unconfirmed
# this is a special case for me because I'm using :confirmable
# the login was correct, but the user hasn't confirmed their
# email address yet
::Rails.logger.info "*** Login Failure: unconfirmed account access: #{email}"
elsif opts[:action] == "unauthenticated"
# "unauthenticated" indicates a login failure
if !user_exists
# bad email:
# no user found by this email address
::Rails.logger.info "*** Login Failure: bad email address given: #{email}"
else
# the user exists in the db, must have been a bad password
::Rails.logger.info "*** Login Failure: email-password mismatch: #{email}"
end
end
end
I expect that you could use the before_logout callback to track logout actions as well, but I haven't tested it. There appear to be prepend_ variants of the callbacks as well.
For logout logging, you need to catch the destroy event, so add the following to the Session controller (from the above answer):
before_filter :log_logout, :only => :destroy #add this at the top with the other filters
def log_logout
::Rails.logger.info "*** Logging out : #{current_user.email} ***\n"
end
I've found another way to do this, if you want, for example, display a custom message if login fails.
In my job, if login fails we check the activity status (custom logic) and display a message, no matter if the login was correct or not.
After debug a little bit and read warden docs I know this now: Warden executes a throw(:warden, opts), so, according to ruby docs, a throw must be captured inside a catch block.
def create
flash.clear
login_result = catch(:warden) { super }
return unless login_failed?(login_result)
email = params[:user][:email]
flash[:alert] = # here I call my service that calculates the message
redirect_to new_user_session_path
end
def login_failed?(login_result)
login_result.is_a?(Hash) && login_result.key?(:scope) && login_result.key?(:recall)
end
throw docs:
https://ruby-doc.org/core-2.6.3/Kernel.html#method-i-throw
catch docs:
https://ruby-doc.org/core-2.6.3/Kernel.html#method-i-catch
Building on Prakash Murty's answer, I think the approach in this answer (https://stackoverflow.com/a/34816998/891359) is a cleaner way to log a succesfull login attempt. Instead of calling super, Devise offers a way to pass a block that is yielded before the view is rendered.
So instead of doing this:
class SessionsController < Devise::SessionsController
def create
super
::Rails.logger.info "\n***\nSuccessful login with email_id : #{request.filtered_parameters["user"]}\n***\n"
end
end
It is cleaner to do:
class SessionsController < Devise::SessionsController
def create
super do |user|
::Rails.logger.info "\n***\nSuccessful login with email_id : #{user.email}\n***\n"
end
end
end
I am working with a Rails 3 App and am needing to adjust the error message on the password resent view. If a user types in an email address and submits it, currently, the app will display this error message:
Email not found
I am needing to change this error message to this:
We don't have an account with that e-mail address. Maybe you used another address?
I know that you can adjust this in the Devise YML file, but am not sure how to do this... any suggestions?
Working Code
class PasswordsController < Devise::PasswordsController
def create
user = User.find_by_email(params[:user][:email])
if user.nil?
flash.now[:notice] = "We don't have an account with that e-mail address. Maybe you used another address?"
end
super
end
end
You could try using a before_filter that checks if the email exists in the datbase and if not the it redirects you to the password reset form with a flash notice
I'd like to test if an email is delivered if I call a controller method with :post. I'll use email_spec so I tried this snipped here: http://rubydoc.info/gems/email_spec/1.2.1/file/README.rdoc#Testing_In_Isolation
But it doesn't work, because I pass an instance of the model-object to the delivery-method and the instance is saved before the delivery.
I tried to create an other instance of the model-object, but then the id isn't the same.
My controller-method looks like this:
def create
#params = params[:reservation]
#reservation = Reservation.new(#params)
if #reservation.save
ReservationMailer.confirm_email(#reservation).deliver
redirect_to success_path
else
#title = "Reservation"
render 'new'
end
end
Do you have any idea to solve this?
Assuming your test environment is set up in the usual fashion (that is, you have config.action_mailer.delivery_method = :test), then delivered emails are inserted into the global array ActionMailer::Base.deliveries as Mail::Message instances. You can read that from your test case and ensure the email is as expected. See here.
Configure your test environment to accumulate sent mails in ActionMailer::Base.deliveries.
# config/environments/test.rb
config.action_mailer.delivery_method = :test
Then something like this should allow you to test that the mail was sent.
# Sample parameters you would expect for POST #create.
def reservation_params
{ "reservation" => "Drinks for two at 8pm" }
end
describe MyController do
describe "#create" do
context "when a reservation is saved" do
it "sends a confirmation email" do
expect { post :create, reservation_params }.to change { ActionMailer::Base.deliveries.count }.by(1)
end
end
end
end
Note that my example uses RSpec 3 syntax.
I know I'm late to the party with this one, but for future Googlers...
I think a better solution to this problem is answered here
The previously accepted answer is testing the Mailer itself (inside the controller spec). All you should be testing for here is that the Mailer gets told to deliver something with the right parameters.
You can then test the Mailer elsewhere to make sure it responds to those parameters correctly.
ReservationMailer.should_receive(:confirm_email).with(an_instance_of(Reservation))
This is way how to test that Mailer is called with right arguments. You can use this code in feature, controller or mailer spec:
delivery = double
expect(delivery).to receive(:deliver_now).with(no_args)
expect(ReservationMailer).to receive(:confirm_email)
.with('reservation')
.and_return(delivery)
Anyone using rspec +3.4 and ActiveJob to send async emails, try with:
expect {
post :create, params
}.to have_enqueued_job.on_queue('mailers')
To add a little more, make sure if you're going to stub out a call using should_receive that you have an integration test elsewhere testing that you're actually calling the method correctly.
I've been bit a few times by changing a method that was tested elsewhere with should_receive and having tests still pass when the method call was broken.
If you prefer to test the outcome rather than using should_receive, shoulda has a nice matcher that works like the following:
it { should have_sent_email.with_subject(/is spam$/) }
Shoulda documentation
More information on using Shoulda Matchers with rSpec
If you're using Capybara with Capybara Email and you sent an email to test#example.com, you can also use this method:
email = open_email('test#example.com')
And then you can test it like this:
expect(email.subject).to eq('SUBJECT')
expect(email.to).to eq(['test#example.com'])
Try email-spec
describe "POST /signup (#signup)" do
it "should deliver the signup email" do
# expect
expect(UserMailer).to(receive(:deliver_signup).with("email#example.com", "Jimmy Bean"))
# when
post :signup, "Email" => "email#example.com", "Name" => "Jimmy Bean"
end
end
more examples here: https://github.com/email-spec/email-spec#testing-in-isolation
I'm trying to localize my users throught their IP address. As the docs say, a class method called geocode_ip_address has been mixed into the ActionController::Base. But there must be something I'm missing. Do I have to define a filter like this before_filter :geocode_ip_address to use it? (I want to know the location for every request done).
The documentation also talks about "A first-time lookup will result in the GeoLoc class being stored in the session as :geo_location" but I certainly don't have that key inside the session hash.
What am I doing wrong?
Thanks.
You don't need to prepend before_filter to geocode_ip_address, but rather just put that in your controller:
class YourController < ApplicationController
geocode_ip_address
def action
if geo = session[:geo_location]
# geo here is a Geokit::GeoLoc object
end
end
end
Note that if the geocoding fails geo will be nil. If you're running in development you'll be trying to geocode 127.0.0.1, so you'll want to set your remote_ip in the request object. I did so by adding this to the bottom of config/environments/development.rb:
class ActionDispatch::Request
def remote_ip
"x.x.x.x" # fill in your IP address here
end
end