Problem: How do I send the user an email after they sign up for the first time with Facebook? I'm using device and omniauth.
I have confirmation emails working for regular signup with devise. I need to send an email when the user gets added to my database for the first time after signing in with Facebook. Where in the code is this happening?
I tried adding a line of code sending the email in my omniauth_callbacks_controller.
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
# omniauth_callbacks_controller
def facebook
#user = User.from_omniauth(request.env["omniauth.auth"])
facebook = "www.facebook.com"
if #user.persisted?
print "User persisted"
sign_in #user, :event => :authentication
set_flash_message(:notice,:success,:kind => "Facebook") if is_navigational_format?
# I SENT THE EMAIL HERE
else
session["device.facebook_data"] = request.env["omniauth.auth"]
redirect_to root_path
end
end
However, this just sends the user a confirmation email EVERY time they log in with Facebook, which is not what I want. I want to simply send the email the first time they log in.
The email should be sent in the registrations_controller. However, when users are signing up with Facebook, this controller is never used.
class RegistrationsController < Devise::RegistrationsController
def create
build_resource(sign_up_params)
if resource.save
if resource.active_for_authentication?
set_flash_message :notice, :signed_up if is_navigational_format?
sign_up(resource_name, resource)
# Tell the UserMailer to send a welcome email after save
UserMailer.welcome_email(current_user).deliver_later
return render :json => {:success => true}
else
set_flash_message :notice, :"signed_up_but_#{resource.inactive_message}" if is_navigational_format?
expire_session_data_after_sign_in!
return render :json => {:success => true}
end
else
clean_up_passwords resource
invalid_signin_attempt
end
end
Would like to know the right way to send a confirmation to the user after signing up with Facebook.
Problem
It looks like your User.from_omniauth function behaves like a find_or_create call. That means that the controller has no knowledge of whether the user was just created or is being fetched from an existing identity in the database.
If the user is created as part of this from_omniauth call, then you should be able to just rely on the Devise :confirmable module. Otherwise, the user is created before you get back the OAuth credentials, so you need to handle it manually.
The code in the from_omniauth function likely looks something like this:
def self.from_omniauth(token)
user = User.find(token: token)
if user.nil?
user = User.create(token: token, ...)
# ...
end
# ...
end
There might be an intermediary Token, Identity, or other such class, but the logic should be the same.
Fix
There are two easy ways to fix this:
Include a created boolean as part of the from_omniauth return value, which the controller can then use to gate the confirmation email on.
Move the "create" part of the "find or create" logic out into the controller, so that the email can be sent as part of the "create" path.
Aside
Also, I'd suggest using the Devise resource.send_confirmation_instructions function and piggybacking your email off that. That way, all welcome emails share the same code and you're not maintaining a separate module just for Facebook/OAuth login.
Related
I have set confirmable in my devise user model but when I try to register email, which is already registered, I get error that email is registered or waiting for confirmation.
This is correct but for security reasons I want to always show something like "confirmation email has been sent to email address" so nobody can figure out which emails are already registered in the app.
Is this somewhere in devise config? Or do I have to manually modify registration controller to not throw any error if email exists in db?
The best way is way is to manually modify from registration controller (https://github.com/heartcombo/devise#configuring-controllers), you can rescue the error and changes the flash message.
I borrowed has_duplicate_email? from this post , checked devise source code and then modified create in registration controller and routes to use it. I hope it helps somebody
def has_duplicate_email?
return false unless resource.errors.has_key?(:email)
resource.errors.details[:email].any? do |hash|
hash[:error] == :taken
end
end
def create
super do |resource|
if has_duplicate_email?
set_flash_message! :notice, :"signed_up_but_#{resource.inactive_message}"
expire_data_after_sign_in!
redirect_to root_path and return
end
end
end
devise_for :users, :controllers => {:registrations => "users/registrations"}
I'm setting up a CRUD feature and I only want a few people (entire email addresses) to be able to register through devise.
I've gone through countless posts but they mostly use email domains or single person login. I've also tried creating my own validation method in my User.rb but I can't seem to get it.
validate :check_email
private
def check_email
#users = User.all
if #users == '123.example#gmail.com')
events_path
else
errors.add(:email, 'is not authorized')
end
end
I don't get an explicit error but the app seems to skip my 'if' condition and outputs the 'else' condition.
To allow only users with certain email addresses you could simply add an inclusion validation.
class User < ApplicationRecord
ALLOWED_EMAILS = %w[
123.example#gmail.com
456.example#gmail.com
789.example#gmail.com
].freeze
validates :email, inclusion: { in: ALLOWED_EMAILS, message: :invalid }
# ...
end
You could also opt to load the ALLOWED_EMAILS from the settings or a file.
To load from the Rails config you have to define the email addresses in a config file.
config.allowed_user_emails = %w[
123.example#gmail.com
456.example#gmail.com
789.example#gmail.com
]
Then load them in the controller using:
ALLOWED_EMAILS = Rails.configuration.allowed_user_emails.freeze
To load from for example a yaml file you could do something like:
ALLOWED_EMAILS = YAML.load_file(Rails.root.join('config', 'allowed_user_emails.yml')).freeze
Having the following in the file:
- 123.example#gmail.com
- 456.example#gmail.com
- 789.example#gmail.com
The easiest way to accomplish this would be to create a custom validation like you did originally. In your user model you could create a validation that looks something like this:
validate :is_email_valid?
def is_email_valid
if ["example#gmail.com", "example#yahoo.com", "123.example#gmail.com"].include?(self.email)
errors.add :base, "Your email is not authorized for use!"
end
end
This code will work when saving or creating a new record.
The problem with your code is that you are attempting to validate on a ActiveRecord_Relation object which cannot directly access validations for class instances. It's like trying to call an instance method from a class level; you have to validate one user at a time. You will want to execute validations on your object instance with a reference to self. So, looping through your users and then validating would work. Here is an example:
User.all.each do |i|
if i.valid?
puts "VALID"
else
puts "INVALID"
end
end
Sarah,
The first step that I'd take in accomplishing this would be to work inside devise's registrations_controller. This is the controller where the signed up user will be served to and here you will want to over-write some of the Devise code.
Ensure the controllers are created first by running Devise's controller generator:
rails generate devise:controllers users
Then you'll want to find the user's registrations_controller.rb and override the code in there under the create action.
Here's an example of some code I wrote to override Devise's admin controller:
def create
build_resource(sign_up_params)
# Below - If admin is coming from an email invite or shared invite link, grabs both token and workplace id from params.
#token = params[:invite_token] if params[:invite_token]
#workplace_id = params[:workplace_id] if params[:workplace_id]
#workplace = Workplace.find(params[:workplace_id]) if params[:workplace_id] # Finds the workplace from the workplace_id, works for both invite email and shared link.
#institute = #workplace.institute if params[:workplace_id]
if #institute && #institute.has_super_admins?
resource.super_admin = false
end
if resource.save
yield resource if block_given?
if resource.persisted?
if resource.active_for_authentication?
# Below - If admin came from a shared workpalce invite link or email workplace invite
if #token != nil # Admin signed up via a workplace invite email or the shared link
# Below - Checks Payment plan for admins joining an institute to make sure the institute didn't exceed user count subscription permission.
unless #institute.plan.unlimited_users?
if #institute.plan.user_count <= #institute.admins.count # Admin doesn't join workplace since institute has excited user allowance
set_flash_message! :alert, :signed_up_no_workplace, :workplace => #workplace.name, :institute => #institute.name, :workplace_owner => #institute.subscription.admin.name
sign_up(resource_name, resource)
respond_with resource, location: institute_admin_path(resource.institute, resource)
else # Admin successfully signs up and joins the workplace. Method is below in protected
join_workplace_and_redirect
end
else # Admin successfully signs up and joins the workplace. Method is below in protected
join_workplace_and_redirect
end
else # Fresh admin signup
sign_up(resource_name, resource)
if resource.super_admin? # Checks if the admin is a super_admin and set as true, if so redirects to another page
set_flash_message! :notice, :super_admin_signed_up, :name => resource.first_name
respond_with resource, location: new_institute_path()
else # Admin is not a super_admin and not first one who signed up inside a city.
set_flash_message! :notice, :admin_signed_up, :link => edit_institute_admin_path(resource.institute, resource), :city => resource.institute.name
respond_with resource, location: after_sign_up_path_for(resource)
end
end
else
set_flash_message! :notice, :"signed_up_but_#{resource.inactive_message}"
expire_data_after_sign_in!
respond_with resource, location: after_inactive_sign_up_path_for(resource)
end
else
flash[:alert] = "Your profile could not be created. See why below!"
set_flash_message! :alert, :"signed_up_but_#{resource.inactive_message}"
redirect_to request.referrer
end
else # Failed to save
clean_up_passwords resource
respond_with resource, location: new_admin_registration_path(workplace: #workplace)
end
end
To apply it to your case, what you may want to do is use a case statement to see if the email matches ones you want it to. For example, below you'll want to check the resource (the single user in this case, the user who is signing up) email attribute to determine whether it's a success or failure:
# Checks emails that are allowed
case resource.email
when "123.example#gmail.com"
if resource.save # Success
set_flash_message! :notice, :signup_succeed
respond_with resource, location: home_page(resource)
else # For some reason the allowed email didn't go through due to other validations
set_flash_message! :alert, :signup_failure
respond_with resource, location: new_user_path(resource)
end
else # Entered an email that is not allowed
set_flash_message! :alert, :email_invalid
respond_with resource, location: new_user_path(resource)
end
The set_flash_message! is Devises custom messages which can be edited in config/locales/devise.en.yaml. The second keyword refers to the name of the yaml key, and you can customize the error or success message in there. respond_with is the redirection and location. You can use as many when statements as needed. This is just one way to do it.
Hope this helps.
I've seen a couple people ask a similar question, but I really need advice on how to debug this issue. I'm trying to setup facebook connect using Devise using the article here: https://github.com/plataformatec/devise/wiki/OmniAuth:-Overview
Every time I click on the login with facebook link, I get the blank page that just says: Not found. Authentication passthru. Clearly, there is no JavaScript/ajax setup on the prior page to pull up the facebook login screen.
I know this can work on my system, as I made a blank project with the exact same code from the link above and it works. Of course, my project is much bigger with lots of code, so I'm trying to figure out what in my project is causing this not to fire.
Any help on how to debug is appreciated.
Thanks!
This is the #passthru code from the devise source.
def passthru
render :status => 404, :text => "Not found. Authentication passthru."
end
Which means that devise is unable to recognize your facebook callback. Make sure you setup up your callback controller properly or post your user controller code.
I ran into the same error. I was missing the Facebook callback controller (app/controllers/users/omniauth_callbacks_controller.rb):
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
# ...
def facebook
respond_to do |format|
format.html {
#user = User.find_for_facebook(request.env["omniauth.auth"])
if #user.persisted?
sign_in_and_redirect #user
set_flash_message(:notice, :success, :kind => "Facebook") if is_navigational_format?
else
session["devise.facebook_data"] = request.env["omniauth.auth"]
redirect_to new_user_registration_url
end
}
end
end
end
This code references the method "find_for_facebook" in my user model (app/models/user.rb):
class User < ActiveRecord::Base
# ...
def self.find_for_facebook(auth_hash)
user = User.where(:email => auth_hash.info["email"]).first
unless user
user = User.create(
username: auth_hash.info["nickname"],
email: auth_hash.info["email"],
password: Devise.friendly_token[0,20])
end
user.provider = auth_hash["provider"]
user.uid = auth_hash["uid"]
user
end
end
Make sure to restart your development server so all the changes get picked up.
Here is my user session controller create action that the login goes through
# POST /resource/sign_in
def create
resource = User.find_by_email(params[:user][:email])
# check for inactive
redirect_to(new_user_session_path, :notice => 'Invalid Email Address or Password.') and return if resource.try(:active) == false
# check to see if user is AD user
if ad_resource?(resource)
if !ActiveDirectory.new.authenticate!(params[:user][:email], params[:user][:password])
redirect_to new_user_session_path, :notice => 'Invalid Email Address or Password.'
return
end
else
resource = warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#new")
end
set_flash_message :notice, :signed_in
sign_in_and_redirect(resource_name, resource)
end
this line
resource = warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#new")
How can that possible login anyone...I am not as familiar to devise and i am trying to login and i know there is an active user in the db but i cant log in and looking at the warden.authenticate line confuses me because it doensnt pass in the email and password...any help would be great to help me understand what is happening in the authentication
There is a warden initializer file its run as middleware that picks up the correct params when used with devise...
Have a look at the warden gem to understand how it works fully.
Personally i find devise gets in the way very quickly and you might be better understanding how it all works if you 'roll' your own authentication system.
I currently use Devise for user registration/authentication in a Rails project.
When a user wants to cancel their account, the user object is soft deleted in a way like the following.
How to "soft delete" user with Devise
My implmenetation has a small difference this way.
User model has an attribute 'deleted_flag'.
And, soft_delete method executes "update_attribtue(:deleted_flag, true)"
But, I have to implment sign_in action.
In my implmenetation is the following.
class SessionsController < Devise::SessionsController
def create
resource = warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#new")
if resource.deleted_flag
p "deleted account : " + resource.deleted_flag.to_s
sign_out(resource)
render :controller => :users, :action => :index
else
if is_navigational_format?
if resource.sign_in_count == 1
set_flash_message(:notice, :signed_in_first_time)
else
set_flash_message(:notice, :signed_in)
end
end
sign_in(resource_name, resource)
respond_with resource, :location => redirect_location(resource_name, resource)
end
end
end
I think this code has strange points.
If deleted user tries to sing in,
the system permit logging and make log out immediately.
And, the system cann't display flash[:alert] message...
I want to know two points.
How do I implement to prohibit deleted users to login?
How do I implement to display flash[:alert] when deleted user tries to login?
To stop a user that has been 'soft deleted', the best way is to overwrite the find_for_authentication class method on the user model. Such as:
Class User < ActiveRecord::Base
def self.find_for_authentication(conditions)
super(conditions.merge(:deleted_flag => false))
end
This will generate a invalid email or password flash message by devise (because it cannot find the user to authenticate)
As far as your second question though, you'll need some for of method in your controller to add a particular flash message. However, in my opinion you should treat users that are 'soft' deleted the same as if they didn't exist in the database at all. Thus if they tried to log in, they should just get an valid email or password message.
See my solution here: https://stackoverflow.com/a/24365051/556388
Basically you need to override the active_for_authentication? method on the devise model (User).
I haven't tried anything like that but it seems if you want to catch the user before authentication you'll either have to write a Devise authentication strategy or a before_filter to be run before authenticate_user!. Something like:
before_filter :no_deleted_users
def no_deleted_users
if User.find(params[:email]).deleted?
redirect_to root_path, :flash => { :error => "Your user was deleted. You cannot log in." }
end
end
Although it might be more complex to get the user than that. I haven't played with Devise pre-authentication.
The modern and correct answer is this:
class User < ApplicationRecord
def active_for_authentication?
super && !discarded? # or whatever...
end
end
See the documentation here.