Rails unusual statement: where( ).first_or_create( ) - ruby-on-rails

I am following Ryan's Omniauth with Devise railscast. A part of the code was:
class User < ActiveRecord::Base
def self.from_omniauth(auth)
where(auth.slice(:provider, :uid)).first_or_create do |user|
user.provider = auth.provider
user.uid = auth.uid
user.username = auth.info.nickname
end
end
end
I am scratching my head with where().first_or_create do. What does this code exactly do?
My guess is that where() statement is applied to class User. But how does Rails know that it is equivalent to User.where(:provider => "provider_id passed by auth", :uid => "uid passed by auth")?
My guess is that it yields a collection, then if collection is empty, then create a new instance user and assign new attributes. Else pick the first instance and reassign attributes.
Are my guesses correct?

Let's slice this up into parts.
auth.slice(:provider, :uid)
auth is a hash that contains a :provider and :uid keys. calling .slice to auth returns a new hash with only these as keys. ie
auth = { provider: 'foo', uid: 'bar', some_other_key: 'blah' }
new_hash = auth.slice(:provider, :uid) # { provider: 'foo', uid: 'bar' }
first_or_create
is the equivalent of find_or_create_by. You can read the documentation and see that it searches for any record that matched the options passed to where. If no record is found, it tries to create a record with those options as attributes.
the block passed
user.provider = auth.provider
user.uid = auth.uid
user.username = auth.info.nickname
actually only needs the last line. The first 2 lines are redundant because it is in the where option.

Related

How to insert data into two tables in rails at the same time?

I am trying from last 2 days to figure out this problem but no avail till now. I am working on omniauth in rails and I have integrated the omniauth authentication in my site but I want to create seperate table for all providers.I have two tables 1: user and other is auth_provider
1: user will contain user data
2: auth_provder will contain user_id with social media provider authentication details.
I have created has_many association from user to auth_providers.
Now I want to insert the data in both tables so that first the would create entry in user table with some fields and then it should take the newly insert record entry and insert this id with other data into auth_providers table. I know this thing is possible but I am unable to find any example or code. I am just searching and trying from last 2 days but still no success. I want to do it using associations and I have also used nested_attributes but still no success.
Here is my code
user model:
class User < ApplicationRecord
# Include default devise modules. Others available are:
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :confirmable,
:omniauthable, :omniauth_providers => [:facebook,:twitter,:linkedin]
has_many :organizations_users
has_many :auth_providers
has_many :organizations, through: :organizations_users
accepts_nested_attributes_for :auth_providers
def active_for_authentication?
# Uncomment the below debug statement to view the properties of the returned self model values.
super && self.active && self.exp_alert == false
end
def self.from_omniauth(auth)
exist = where(email: auth.info.email).first
if exist
existing_user = exist["id"]
Auth_provider.where(provider: auth.provider, social_uid: auth.uid).first_or_create do |auth_provider|
auth_provider.provider = auth.provider
auth_provider.social_uid = auth.uid
auth_provider.social_token = auth.credentials.token
auth_provider.user_id = existing_user
end
else
##organization.users.create(first_name:params[:first_name])
names = auth.info.name.strip.split(" ")
first_name = names[0]
last_name = names[1]
params = { user: {
email: 'abc#abc.com', auth_providers_attributes: [
{ provider: 'facebook' },
{ social_token: 'asfasf2342432' },
{ social_uid: 'asfdaf23242'} # this will be ignored
]
}}
user = User.create(params[:user])
end
end
def self.new_with_session(params, session)
super.tap do |user|
if data = session["devise.facebook_data"] && session["devise.facebook_data"]["extra"]["raw_info"]
user.email = data["email"] if user.email.blank?
end
end
end
def self.find_or_create_from_auth_hash(auth_hash)
user = where(provider: auth_hash.provider, uid: auth_hash.uid).first_or_create do |user|
user.first_name = auth_hash.info.nickname
user.active = 'true'
user.admin=='false'
user.exp_alert = 'false'
user.password = Devise.friendly_token[0,20]
user.token = auth_hash.credentials.token
user.email = auth_hash.info.email
user.secret = auth_hash.credentials.secret
user.skip_confirmation!
end
user
end
def self.linkedin_hash(auth_hash)
user = where(provider: auth_hash.provider, uid: auth_hash.uid).first_or_create do |user|
user.first_name = auth_hash.info.first_name
user.last_name = auth_hash.info.last_name
user.active = 'true'
user.admin = 'false'
user.exp_alert = 'false'
user.password = Devise.friendly_token[0,20]
user.token = auth_hash.credentials.token
user.email = auth_hash.info.email
user.skip_confirmation!
end
user
end
def inactive_message
"Your Account has not been active yet."
end
def after_confirmation
super
self.update_attribute(:active, true)
end
end
auth_providers model
class Auth_provider < ApplicationRecord
devise :database_authenticatable
belongs_to :user
accepts_nested_attributes_for :user
end
Response from facebook is this
#<OmniAuth::AuthHash credentials=#<OmniAuth::AuthHash expires=true expires_at=1517924507 token="EAAdDsC4F0CCLq95eo81qfaMVVs0zeNgqtRqUF9ufZB3gK609NY3aiikJ9AvE8zSB63WFcG0E6NBFNIWf00DjgHNlsQHCd2D26uxJ1ongQ5YBJZCeuZAOas2SEYlRwPYhctfiEVVdbadOyA3QeL50JHIA5dKa3xdfK5Efw9Y"> extra=#<OmniAuth::AuthHash raw_info=#<OmniAuth::AuthHash email="testing#gmail.com" id="35232364989259" name="John">> info=#<OmniAuth::AuthHash::InfoHash email="testing#gmail.com" image="http://graph.facebook.com/v2.6/35489/picture" name="John"> provider="facebook" uid="354814524989259">
If I'm understanding you right, could you do something like:
...
user = User.create(the_user_attributes)
user.auth_providers.create(the_auth_provider_attributes)
...
This should work, creating a user, then an auth provider belonging to them.
You can also use User.new and user.auth_providers.build if you need to manipulate either a little ahead of saving, and they'll be persisted when you call save on the user.
Update
To split the code into readable chunks, I'd suggest something like the following:
user.rb
# update: you're in the user model, so no need to specify the model again in the line below
user = create(user_attributes_from_auth(auth))
user.auth_providers.create(provider_attributes_from_auth(auth))
private
def user_attributes_from_auth(auth)
{ first_name: auth.info.nickname,
token: auth_hash.credentials.token,
etc: etc }
end
def provider_attributes_from_auth(auth)
{ a_similar_approach_as: user_attributes_from_auth }
end
So, in a nutshell, you use the first two lines in there ^^ to create a user from a hash of attributes, then do the same for the auth provider.
This is nice and readable, and not actually far off what you're currently doing. Have a go and see how you get on - hope it helps!

Rails: Cookie overflow with omniauth twitter sign up

I am using omniauth to let people sign up/sign in with Facebook and its working well ! But I wanted to add the omniauth-twitter gem to let them connect with Twitter.
I followed the same steps than when I set up the Facebook connect: https://github.com/plataformatec/devise/wiki/OmniAuth:-Overview
But when I signing up/in I get the following error:
ActionDispatch::Cookies::CookieOverflow in OmniauthCallbacksController#twitter
at the following URL:
http://localhost:3000/users/auth/twitter/callback?oauth_token=HRjON8J4bj9EcbjiELHcpHmSXo0cPd0wCHyuWG8ATZU&oauth_verifier=ZiZb1FAKZmNML1gVu5RKBLEGzbeAPPzC80QCpPDGU
I tried different things suggested on similar posts but none of these worked :(
Here is my configuration:
omniauth_callbacks_controller.rb => app/controllers/omniauth_callbacks_controller.rb
def twitter
# You need to implement the method below in your model (e.g. app/models/user.rb)
#user = User.find_for_twitter_oauth(request.env["omniauth.auth"])
if #user.persisted?
sign_in_and_redirect #user, :event => :authentication #this will throw if #user is not activated
set_flash_message(:notice, :success, :kind => "twitter") if is_navigational_format?
else
session["devise.twitter_data"] = request.env["omniauth.auth"]
redirect_to new_user_registration_url
end
end
user.rb => app/models/user.rb
def self.find_for_twitter_oauth(auth)
where(auth.slice(:provider, :uid)).first_or_create do |user|
user.provider = auth.provider
user.uid = auth.uid
user.email = auth.info.email
user.password = Devise.friendly_token[0,20]
user.name = auth.info.name # assuming the user model has a name
end
end
def self.new_with_session(params, session)
super.tap do |user|
if data = session["devise.twitter_data"] && session["devise.twitter_data"]["extra"]["raw_info"]
user.email = data["email"] if user.email.blank?
end
end
end
devise.rb => app/config/initializers/devise.rb
Rails.application.config.middleware.use OmniAuth::Builder do
provider :twitter, "KEY, "KEYPASSWORD
end
Any ideas what's wrong?
As Michael says in the comments, you're storing a large hash in the session and it's too big (you're using the default CookieStore and cookies can only contain 4KB of data). That hash provided by omniauth has all the data returned by twitter, which can be quite a lot. For example, see the README: https://github.com/arunagw/omniauth-twitter#authentication-hash
If the code in your question is all the code relating to twitter login, then it looks like you only need to keep the email in the session as that is all that is used by your new_with_session code. So your line in the else in twitter which is:
session["devise.twitter_data"] = request.env["omniauth.auth"]
could be something like:
session["devise.twitter_data"] = request.env["omniauth.auth"].select { |k, v| k == "email" }
However the major flaw with this is that twitter doesn't return an email address for a user, so data["email"] will always be nil in new_with_session anyway! So it's pointless keeping anything in the session if you are only later interested in the email which is never returned by twitter. Perhaps you instead want to retrieve a name to help prefill the registration form instead of the email address. In this case, you could just keep that in the hash from omniauth. If you want to keep a few things in the hash, then instead of selecting them all to put in the session, you could do something like:
session["devise.twitter_data"] = request.env["omniauth.auth"].delete_if("extra")
which will remove the "extra" nested hash which could help everything else to fit in the session.
For a complete solution you'll have to consider messy situations like dealing with people who have signed in with Facebook and then come and sign in with Twitter and want to use the same email address and merge with their existing account on your system.
In any case, note that if you are using Rails 3 then the session cookie is not encrypted so the user or anyone with access to their computer could read the contents of the cookie with whatever data from twitter you end up keeping in there. If you're using Rails 4, then the cookie should be encrypted to protect against that.

registration of new users with Devise and omniauth-google-oauth2

I've trying to manage user sign up with google account for my rails 4.0.0 app. Devise works perfectly. And there is working sign in with Google Account for existing users. But I have some difficulties with new user registration using Google Oauth 2. For example: i've got google account "example#google.com". It's logged in on my current PC. And when I try to sign up with this account to my app it generates blank register form. If I dont manually provide email, login, full name, etc. - I've got error message that they "cannot be blank". I guess solution is create default value to text fields to fetch user details.
So, my question is how can I provide values for variables in view that equals variables from google account?
Email field in form_for in new user registration:
= f.email_field :email, :autofocus => true, :value => 'how can i put auth.info.email here?'
omniauth_callbacks_controller.rb:
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def google_oauth2
user = User.from_omniauth(request.env["omniauth.auth"])
if user.persisted?
flash.notice = "Signed in Through Google!"
sign_in_and_redirect user
else
session["devise.user_attributes"] = user.attributes
flash.notice = "You are almost Done! Please provide a password to finish setting up your account"
redirect_to new_user_registration_url
end
end
end
omniauth method from user model:
def self.from_omniauth(auth)
if user = User.find_by_email(auth.info.email)
user.provider = auth.provider
user.uid = auth.uid
user
else
where(auth.slice(:provider, :uid)).first_or_create do |user|
user.provider = auth.provider
user.uid = auth.uid
user.full_name = auth.info.name
user.email = auth.info.email # THIS (user.email) value i want to provide to my registration form as default value
user.birthday = auth.info.birthday
user.avatar = auth.info.image
end
end
end
I had the same problem with GitHub you can take a look at my user model
https://github.com/flower-pot/pastebin/blob/master/app/models/user.rb

how to pass username from omniauth to stripe customer create

I'm trying to pass the username that is collected from Twitter or facebook when my users get authenticated and created, however a simple (description = user.username) doesn't seem to do it, here's what I've got so far;
def self.create_from_omniauth(auth)
create! do |user|
user.provider = auth["provider"]
user.uid = auth["uid"]
user.username = auth["info"]["nickname"]
#user.email = auth["info"]["email"]
user.save!
if user.save
Stripe::Customer.create(description = user.username)
end
user
end
end
I keep getting the following error, no matter if I use #user.username or even current_user.username.
undefined method `each' for "xhtmlit":String
I was completely doing it wrong which is why it was spitting out the error for me. The actual code might not be clean but I'm currently working to try and figure out from starting with dirty version to clean version. Here's what I've got so far that works;
if user.save
Stripe::Customer.create(:email => user.username)
end

What is the simplest way to add an profile image with the Omniauth-Facebook Gem?

I'm working with the Omniauth Facebook Gem with Rails and need a profile pick on my app for each user.
Is it as simple as retrieveing the Name of the user from facebook with the Gem, or does it need to be uploaded to Amazon S3 servers etc.?
My user model:
def self.from_omniauth(auth)
where(auth.slice(:provider, :uid)).first_or_initialize.tap do |user|
user.provider = auth.provider
user.uid = auth.uid
user.name = auth.info.name
user.oauth_token = auth.credentials.token
user.oauth_expires_at = Time.at(auth.credentials.expires_at)
user.save!
end
Can't find a clear answer.
Updated:
In my show page it only shows the url:
<b>IMAGE:</b> <%= #user.image %>
Thanks
It is simple to do. And you don't require to store image on your server. You can simply fetch it from facebook.
Here is how I do it:
Add an image field in your User model:
rails generate migration addImageToUsers image:string
Add it attr_accessible list.
Then in your above method, add following listing for image:
user.image = auth.info.image
This is the direct url of where facebook stores the image of the user.
You can inspect your auth hash to study different size of user image.

Resources