Devise Invitable : Optionally Send Email - ruby-on-rails

in devise invitable, you can invite a new user by performing:
User.invite!(:email => "new_user#example.com", :name => "John Doe")
What I would like to do is (sometimes) prevent devise invitable from sending out an email. I found the following code in the library:
def invite!
if new_record? || invited?
self.skip_confirmation! if self.new_record? && self.respond_to?(:skip_confirmation!)
generate_invitation_token if self.invitation_token.nil?
self.invitation_sent_at = Time.now.utc
save(:validate => false)
::Devise.mailer.invitation_instructions(self).deliver
end
end
Any ideas on how to best update that to not send out the email on the last line? I'm not familiar with the ::
thanks

you can use:
User.invite!(:email => "new_user#example.com", :name => "John Doe") do |u|
u.skip_invitation = true
end
or
User.invite!(:email => "new_user#example.com", :name => "John Doe", :skip_invitation => true)
this will skip invitation email.

In your invitations_controller (there should already be one that inherits from Devise::InvitationsController), you can add the following
# this is called when creating invitation
# should return an instance of resource class
def invite_resource
if new_record? || invited?
self.skip_confirmation! if self.new_record? && self.respond_to?(:skip_confirmation!)
super
end
end
This will override Devise's method for inviting, and then call the original Devise method (super) only if the condition is met. Devise should then handle the token generation and send the invite. You may also want to setup what the app does if the condition is false, in my case that looks like this:
def invite_resource
if user_params[:is_free] == "true"
super
else
# skip sending emails on invite
super { |user| user.skip_invitation = true }
end
end
when params[:is_free] is set to ''true'' the invitation is sent, otherwise the resource is created, but no invitation is sent.
After some digging I found this solution here: https://github-wiki-see.page/m/thuy-econsys/rails_app/wiki/customize-DeviseInvitable

Related

Devise not confirming email on update

I am using devise for authentication. I am overwriting devise token generator so that I can use 6 digit code and also overwriting it so that I can support mobile number confirmation.
If a user register with email and OTP is send via email. Registration seems to work fine. A user register with an email. An OTP is sent and after confirmation a user gets confirmed.
But when the user tries to update the email. I am using the same methods to send the confirmation code (as in registration which works fine) the user get saved in unconfirmed_email. A mail gets send in email but after confirmation a user email is not being copied to email field from unconfirmed_email field.
What could be the problem here.
app/services/users/confirmation_code_sender.rb
# frozen_string_literal: true
module Users
class ConfirmationCodeSender
attr_reader :user
def initialize(id:)
#user = User.find(id)
end
# rubocop :disable Metrics/AbcSize
def call
generate_confirmation_token!
if user.email?
DeviseMailer.confirmation_instructions(
user,
user.confirmation_token,
{ to: user.unconfirmed_email || user.email }
).deliver_now
else
Telco::Web::Sms.send_text(recipient: user.unconfirmed_mobile || user.mobile_number, message: sms_text)
end
end
# rubocop :enable Metrics/AbcSize
private
def generate_confirmation_token!
user.confirmation_token = TokenGenerator.token(6)
user.confirmation_sent_at = DateTime.current
user.save!(validate: false)
end
def sms_text
I18n.t('sms.confirmation_token', token: user.confirmation_token)
end
end
end
app/services/users/phone_or_email_updater.rb
# frozen_string_literal: true
module Users
class PhoneOrEmailUpdater < BaseService
def call
authorize!(current_user, to: :user?)
current_user.tap do |user|
user.update!(unconfirmed_mobile: params[:unconfirmed_mobile], unconfirmed_email: params[:unconfirmed_email])
ConfirmationCodeSender.new(id: user.id).call
end
end
end
end
config/nitializers/confirmable.rb
# frozen_string_literal: true
# Overriding this model to support the confirmation for mobile number as well
module Devise
module Models
module Confirmable
def confirm(args = {})
pending_any_confirmation do
return expired_error if confirmation_period_expired?
self.confirmed_at = Time.now.utc
saved = saved(args)
after_confirmation if saved
saved
end
end
def saved(args)
#saved ||= if pending_reconfirmation?
skip_reconfirmation!
save!(validate: true)
else
save!(validate: args[:ensure_valid] == true)
end
end
def pending_reconfirmation?
if unconfirmed_email.present?
self.email = unconfirmed_email
self.unconfirmed_email = nil
true
elsif unconfirmed_mobile.present?
self.mobile_number = unconfirmed_mobile
self.unconfirmed_mobile = nil
true
else
false
end
end
private
def expired_error
errors.add(
:email,
:confirmation_period_expired,
period: Devise::TimeInflector.time_ago_in_words(self.class.confirm_within.ago)
)
false
end
end
end
end
Mobile update seems to be working fine but email is not updating. I am using graphql to update the email
In console I tried using .confirm but it seems to be not working as well the user email is not getting confirmed
In your pending_reconfirmation?, self.unconfirmed_email is assigned to be nil. It seems like pending_reconfirmation? is only called in saved, however, it is called by pending_any_confirmation, too.
https://github.com/heartcombo/devise/blob/8593801130f2df94a50863b5db535c272b00efe1/lib/devise/models/confirmable.rb#L238
# Checks whether the record requires any confirmation.
def pending_any_confirmation
if (!confirmed? || pending_reconfirmation?)
yield
else
self.errors.add(:email, :already_confirmed)
false
end
end
So when the second time the pending_reconfirmation? is called in the saved, pending_reconfirmation? will return false because unconfirmed_email is nil.
You'd better not do actual assignments inside the methods end with ? it will be an implicit side-effect. Maybe create a new method end with ! to change the value of attributes.
For example:
module Devise
module Models
module Confirmable
def confirm(args = {})
pending_any_confirmation do
return expired_error if confirmation_period_expired?
self.confirmed_at = Time.now.utc
saved = saved(args)
after_confirmation if saved
saved
end
end
def saved(args)
#saved ||= if pending_reconfirmation?
reconfirm_email! if unconfirmed_email.present?
reconfirm_mobile! if unconfirmed_mobile.present?
skip_reconfirmation!
save!(validate: true)
else
save!(validate: args[:ensure_valid] == true)
end
end
def pending_reconfirmation?
unconfirmed_email.present? || nconfirmed_mobile.present?
end
def reconfirm_email!
self.email = unconfirmed_email
self.unconfirmed_email = nil
end
def reconfirm_mobile!
self.mobile_number = unconfirmed_mobile
self.unconfirmed_mobile = nil
end
private
def expired_error
errors.add(
:email,
:confirmation_period_expired,
period: Devise::TimeInflector.time_ago_in_words(self.class.confirm_within.ago)
)
false
end
end
end
end

How to use Devise lockable functionality outside of devise controller or method?

I want to lock the user after 5 multiple attempts in my application. So it is working when i use devise lockable. But i want to use it for API method except default action.
when i am using devise controller & model it is working
devise :database_authenticatable, :registerable, :lockable
user_sign_in method in my controller #using own method not working in my controller
if user.first.valid_password?(params[:password])
user.update_attributes(current_sign_in_at: DateTime.current)
success_response(:ok, user, message: 'success message')
else
error_response(412, nil, 'failure message')
end
in routes
post '/user_sign_in' => 'api/users#user_sign_in'
if i am using api call 'user_sign_in' method. It is not updating devise lockable method. How to trigger devise method in API?
#You Can Try this way. It will work Perfectly
if user.failed_attempts >= Devise.maximum_attempts
user.lock_access!(send_instructions: true)
else
user.increment_failed_attempts
end
I have added /lib/devise/models/lockable.rb files in my application. i have used the below methods based on my lockable functionalities. It is working fine.
def lock_access!(opts = { })
self.locked_at = Time.now.utc
if unlock_strategy_enabled?(:email) && opts.fetch(:send_instructions, true)
send_unlock_instructions
else
save(validate: false)
end
end
def send_unlock_instructions
raw, enc = Devise.token_generator.generate(self.class, :unlock_token)
self.unlock_token = enc
save(validate: false)
send_devise_notification(:unlock_instructions, raw, {})
raw
end
def access_locked?
!!locked_at && !lock_expired?
end
def increment_failed_attempts
self.class.increment_counter(:failed_attempts, id)
reload
end
def unlock_access_by_token(unlock_token)
original_token = unlock_token
unlock_token = Devise.token_generator.digest(self, :unlock_token, unlock_token)
lockable = find_or_initialize_with_error_by(:unlock_token, unlock_token)
lockable.unlock_access! if lockable.persisted?
lockable.unlock_token = original_token
lockable
end
To increment lock count you would use user.increment_failed_attempts. However, to see whether the user is locked or not you would use user.access_locked?.
The sample code is here:
if user.access_locked?
return redirect_to root_path, alert: 'Your account is locked'
end
unless user.valid_password?(params[:password])
if Devise.maximum_attempts <= user.increment_failed_attempts
user.lock_access!(send_instructions: true)
end
user.save(validate: false)
end

Different devise models using same email id to register

I have created two different devise models and it's working fine . But the issue i am facing now is , that both the users are able to register with the same email id . I am looking to an easy fix for it but haven't been able to find one . Any suggestions on the same would be much welcome .
VIEW CODE
<li>
<label>Email Address</label>
<%= f.email_field :email,:class => 'wh-txt-box' , :validate => { :presence => true }, :placeholder => 'Email address ' %>
</li>
This is a simple fix i suggest
class Usera
validate :unique_email
private
def unique_email
errors.add(:email, 'This Email is taken') unless Userb.where(email: self.email).blank?
end
end
class Userb
validate :unique_email
private
def unique_email
errors.add(:email, 'This Email is taken') unless Usera.where(email: self.email).blank?
end
end
You can create a custom Validation method for checking the Uniqueness of Email.
validate :unique_email
private
def unique_email
user = User.where(email: email)
# user.exists? insure you that this email is already present in
# your system. Now you need to validate that If it is a new record (new_record?)
# then show error or at the time of update (read_attribute(:email) != self.email)
# it will also validate that you are entering a unique email.
if user.exists?
if new_record? || read_attribute(:email) != self.email
errors.add(:user, 'This email has already been taken.')
end
end
end

Rails 3.* Devise Facebook OmniAuth intermittently fails with NoMethodError

I followed the steps that are described in https://github.com/plataformatec/devise/wiki/OmniAuth:-Overview and have a method in user model like this:
def self.find_for_facebook_oauth(access_token, signed_in_resource=nil)
data = access_token.extra.raw_info
if user = self.find_by_email(data.email)
user
else # Create a user with a stub password.
self.create!(:email => data.email, :password => Devise.friendly_token[0,20])
end
end
I intermittently get errors like
A NoMethodError occurred in omniauth_callbacks#facebook:
undefined method email' for "false":String
app/models/user.rb:138:infind_for_facebook_oauth'
that I haven't been able to reproduce. What is the source of this problem?
I am not sure what causes this either. Here's a work-around that simply creates a new object:
def self.find_for_facebook_oauth(access_token, signed_in_resource=nil)
data = access_token.extra.raw_info
if data == "false"
self.new
elsif user = self.find_by_email(data.email)
user
else # Create a user with a stub password.
self.create!(:email => data.email, :password => Devise.friendly_token[0,20])
end
end
The controller code shown in the example will then work - it will redirect the user to sign up.

Override mailer in devise_invitable?

I would like the invitations for my app to come from the inviter instead of a system email address. How can I override the config.mailer_sender from devise.rb?
I have this in my mailer and have confirmed that it is getting called, but it does not override the :from. Note: it is a private method, I tried it as a public method with no effect.
private
def headers_for(action)
if action == :invitation_instructions
headers = {
:subject => "#{resource.invited_by.full_name} has invited you to join iTourSmart",
:from => resource.invited_by.email,
:to => resource.email,
:template_path => template_paths
}
else
headers = {
:from => mailer_sender(devise_mapping),
:to => resource.email,
:template_path => template_paths
}
end
if resource.respond_to?(:headers_for)
headers.merge!(resource.headers_for(action))
end
unless headers.key?(:reply_to)
headers[:reply_to] = headers[:from]
end
headers
end
The better solution without any hacks/monkey patches will be:
for example, in your model:
def invite_and_notificate_member user_email
member = User.invite!({ email: user_email }, self.account_user) do |u|
u.skip_invitation = true
end
notificate_by_invitation!(member)
end
def notificate_by_invitation! member
UserMailer.invited_user_instructions(member, self.account_user, self.name).deliver
end
In the mailer:
def invited_user_instructions(user, current_user, sa)
#user = user
#current_user = current_user
#sa = sa
mail(to: user.email, subject: "#{current_user.name} (#{current_user.email}) has invited you to the #{sa} account ")
end
So you can put any subject/data in the body of the mail.
Good luck!
Look at my answer to a similar question, it might help.
Edit: so it seems that you need to define a public headers_for method in your resource class.
Solution: Put some version of this method in User.rb, make sure it's public.
def headers_for(action)
action_string = action.to_s
case action_string
when "invitation" || "invitation_instructions"
{:from => 'foo#bar.com'}
else
{}
end
end
You have to return a hash in because Devise::Mailer will try to merge the hash values.
Take a look at devise_invitable wiki.
class User < ActiveRecord::Base
#... regular implementation ...
# This method is called interally during the Devise invitation process. We are
# using it to allow for a custom email subject. These options get merged into the
# internal devise_invitable options. Tread Carefully.
#
def headers_for(action)
return {} unless invited_by && action == :invitation_instructions
{ subject: "#{invited_by.full_name} has given you access to their account" }
end
end

Resources