first_or_create method with embeds_many association in mongoid - ruby-on-rails

I have User model which used for omni-auth authentication. I have set it up so that different providers (like facebook, google accounts ) can be used for signing in. If the email is already in the database while signing up using these providers ,user is signed into that account.
class User
include Mongoid::Document
embeds_many :providers
field :email,type: String, default: ""
end
class Provider
include Mongoid::Document
field :name, type: String
field :uid, type: String
embedded_in :user
end
And I am using from_omniauth method to find user based on the auth info.
def from_omniauth(auth)
user=User.find_by(email: auth.info.email)
return user if user
where(:"providers.name" => auth.provider, :"provider.uid" => auth.uid ).first_or_create do |user|
user.email = auth.info.email
user.password = Devise.friendly_token[0,20]
end
end
But I am getting an error when it cant find a user and tries to create one.
Mongoid::Errors::UnknownAttribute:
message:
Attempted to set a value for 'providers.name' which is not allowed on the model User.
But why does it consider this as a dynamic field generation as I have already defined the associations. I tried find_or_initialize method also but same error.
Can someone help me figure it out. Thanks in advance.

There's some magic going on here.
When you are invoking #first_or_create on the above criteria:
where(:"providers.name" => auth.provider, :"provider.uid" => auth.uid ).first_or_create do |user|
user.email = auth.info.email
user.password = Devise.friendly_token[0,20]
end
This is actually getting de-sugared into something more like this:
found_user_with_provider = where(:"providers.name" => auth.provider, :"provider.uid" => auth.uid ).first
if found_user_with_provider.nil?
new_user = User.new
new_user.set(:"providers.name", auth.provider)
new_user.set(:"providers.uid", auth.uid)
new_user.email = auth.info.email
new_user.password = Devise.friendly_token[0,20]
else
found_user_with_provider.email = auth.info.email
found_user_with_provider.password = Devise.friendly_token[0,20]
end
Notice the attempt to set the value of "provider.name" and "provider.uid" as attributes on the user instance - which isn't actually what you want, and is what Mongoid is complaining about.
You actually probably want something more like this:
def from_omniauth(auth)
user = User.find_by(email: auth.info.email)
return user if user
found_user_with_provider = where(:"providers.name" => auth.provider, :"provider.uid" => auth.uid ).first
if found_user_with_provider
found_user_with_provider.update_attributes!(
:email => auth.info.email,
:password => Devise.friendly_token[0,20]
)
return found_user_with_provider
end
User.create!({
:providers => [
Provider.new({
:name => auth.provider,
:uid => auth.uid
})
],
:email => auth.info.email,
:password => Devise.friendly_token[0,20]
})
end
Of course there's still one more problem, and that's that you are setting a #password attribute on your user instance. Make sure to add:
field :password,type: String, default: ""
To your user model - otherwise you'll get the same complaint about dynamic fields as before - just this time about a different field.

Related

Rails: facebook oauth: get user profile link

I have a rails app where users can log through facebook with oauth.
I retrieve their image, email and name. What I'd like to do is scrap their profile link (http://facebook.com/name) and gender (male,female, other)
But It doesn't work for link and gender, here is my integration:
Gemfile
gem 'omniauth-facebook'
User.rb model
def self.from_omniauth(auth)
where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
user.email = auth.info.email
user.password = Devise.friendly_token[0,20]
user.name = auth.info.name # assuming the user model has a name
user.image = auth.info.image # assuming the user model has an image
user.gender = auth.info.gender # assuming the user model has a gender
user.link = auth.info.link # assuming the user modal has a link
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
EDIT
devise.rb
config.omniauth :facebook, "***", "***"
But for all users, gender and link return 'nil'
Any ideas what I could be missing ?
Thanks
The url parameter doesn't work now, the correct name of the field is link
config.omniauth :facebook, 'APP_ID', 'APP_SECRET', {scope: 'email', info_fields: 'email,name,verified,gender,link'}
There is a list of attributes we can get from Facebook:
https://developers.facebook.com/docs/facebook-login/permissions/v2.5#reference-public_profile
https://developers.facebook.com/docs/graph-api/reference/user
You need to specify the scope and fields you want to get when you specify the provider in config/initializers/devise.rb
I.e.:
config.omniauth :facebook, 'APP_ID', 'APP_SECRET', {scope: 'email', info_fields: 'email,name,verified,gender,url'}
Whether or not facebook will give you all the data you ask for is a different question.

Rails, Omniauth and Facebook - "Email can't be blank" issue in production but not development

Very simple question.
I have run into the "Email can't be blank" issue with my rails app/omniauth/facebook login. There are many questions on this, and among the answers is that some users give their telephone number and not their email addresses. However in my case, I have a facebook account open, with an email address that works fine with omniauth in development, but when I use it production it comes back email can't be blank. My credentials are correct.
Can anyone explain how you could have this combination of events?
edit: here is my controller code (keep in mind my devise model is "Member" and there is an associated profile model "User"
def provider
auth = request.env["omniauth.auth"]
member_email_check = Member.find_by_email(auth.info.email)
if member_email_check.present? && User.find_by_member_id(member_email_check.id)
sign_in_and_redirect member_email_check
return false
else
member = Member.where(provider: auth.provider, uid: auth.uid).first_or_create do |member|
member.provider = auth.provider
member.uid = auth.uid
member.email = auth.info.email
end
end
if auth.provider == "facebook"
if member.save
unless User.find_by_member_id(member.id)
User.create(:member_id => member.id,
:full_name => auth.info.first_name + " " + auth.info.last_name,
:first_name => auth.info.first_name,
:last_name => auth.info.last_name,
:email => auth.info.email,
:picture => auth.info.image)
end
sign_in_and_redirect member
else
session["devise.member_attributes"] = member.attributes
redirect_to new_member_registration_url
end
end
end
alias_method :facebook, :provider
end
SOLVED!
config.omniauth :facebook, "<YOUR API KEY>", "<YOUR SECRET KEY>", scope: 'email', info_fields: 'email,name,first_name,last_name,gender'
just added the email scope and info_fields.

Rails save returns true but doesn't save anything

I have some code to find a user for Oauth using Omniauth.
If the user signs in with say Facebook on day 1, changes their email address with FB on day 2 and signs back into my app on day 3 I want to update the email address on file.
The code below should sync email address.
I use AwesomePrint to write the email address to the console and they are different. However save returns true but the email address does not update?
def self.find_or_create_for_oauth(auth)
social_identity = SocialIdentity.find_for_oauth(auth.provider, auth.uid)
email = auth.info.email
if social_identity.nil?
user = find_by_email(email)
if user.nil?
# create user for email with random password
end
social_identity = user.social_identities.create!(
provider: auth.provider,
uid: auth.uid
)
end
user = social_identity.user
ap user.email # email address 1
ap email # email address 2
if user.email != email
user.email = email
ap user.save! # returns true
end
user # still has email address 1
end
Further update:
I removed all console writes except for these two:
if user.email != email
user.email = email
puts user.email
user.save!
puts user.email
end
My console returns these (fake) email addresses: kaylin.waters#wilderman.co.uk &
grover#dickens.us. Save is resetting the email address instead of saving it.
So I decided to rather just post the entire method below in case their is something strange going on:
class User
has_many :social_identities, dependent: :destroy
def self.find_or_create_for_oauth(auth)
social_identity = SocialIdentity.find_for_oauth(auth.provider, auth.uid)
email = auth.info.email
if social_identity.nil?
user = find_by_email(email)
if user.nil?
random_password = generate_password
user = User.new(
email: email,
password: random_password,
password_confirmation: random_password)
user.skip_confirmation!
user.save!
end
social_identity = user.social_identities.create!(
provider: auth.provider,
uid: auth.uid
)
end
user = social_identity.user
if user.email != email
user.email = email
puts user.email
user.save!
puts user.email
end
user
end
end
class SocialIdentity < ActiveRecord::Base
belongs_to :user
validates :user_id, presence: true
validates :provider, presence: true
validates :uid, presence: true,
uniqueness: { scope: [:provider, :deleted_at] }
acts_as_paranoid
def self.find_for_oauth(provider, uid)
where(provider: provider, uid: uid).first
end
end
Are you checking the db state after the method being performed or just relying on these puts?
Sometimes it is good to write puts user.reload.email
Anyways, I would analyze the logs to see if there isn't any validation or a callback being triggered which rollbacks the changes or adheres the process in any other way. Also, I would try to do user.update_column('email', email) to see if that works. update_column skips any validations or callbacks.

How do I get more granular LinkedIn user information using linkedin gem and omniauth?

I want to get a user's position information from their LinkedIn profile (company name, title, etc), but I can't get anything more than basic profile information (id, name, email, headline, and image). I'm guessins this is just a syntax thing since I'm requesting access to all fields upon user's authentication.
from user.rb
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.email = auth.info.email
user.headline = auth.info.headline
**user.company = auth.info.company**
user.avatar = auth.info.image
user.password = SecureRandom.urlsafe_base64(n=6)
user.save!
end
from devise.rb
config.omniauth :linkedin, "abcdefghijk", "abcdefghijk",
# :scope => 'r_basicprofile r_emailaddress rw_nus r_fullprofile r_contactinfo r_network rw_company_admin',
# :fields =>
:scope => 'r_basicprofile r_emailaddress rw_nus r_fullprofile r_contactinfo r_network rw_company_admin',
:fields => ["id", "email-address", "first-name", "last-name",
"headline", "industry", "picture-url", "public-profile-url",
"location", "connections", "skills", "date-of-birth", "phone-numbers",
"educations", "three-current-positions" ]
Do I need to add fields to devise.rb or am I being an idiot and formatting user.company = auth.info.company wrong?
Thanks!
[edit]
okay, found it. The devise.rb field definition is wrong. (Probably outdated, I used the same and it was copied from somewhere.)
Correct name now is just "positions", not "three-current-positions"
After that, auth.extra.info.positions becomes available (with these params: https://developer.linkedin.com/docs/fields/positions)
The format for accessing user info id is:
user.name = auth["info"]["name"]

Rails - how to avoid validation errors in creating a record in a model?

I am working on Facebook authentication for Devise and I stumbled upon one problem - when will try to sign up someone with Facebook email that is already in the system, I get an error (which is partially correct).
I would need to redirect this user back to the homepage and there to print out this issue, but at the moment I am having this message printed as an error (on localhost).
Here's how I am doing that (user model):
def self.find_for_facebook_oauth(access_token, signed_in_resource=nil)
data = access_token.extra.raw_info
if user = User.where(:provider => 'facebook', :uid => data.id).first
user
else # Create a user with a stub password.
user = User.create!(:first_name => data.first_name,
:last_name => data.last_name,
:email => data.email,
:password => Devise.friendly_token[0,20],
:provider => 'facebook',
:uid => data.id,
:terms_of_use => true)
end
return user if user
end
How to redirect the user on a page where would be printed out the validation messages?
Thanks
Why don't you have something like this in your model and controller
Return just the user object from the method
def self.find_for_facebook_oauth(access_token, signed_in_resource=nil)
data = access_token.extra.raw_info
#Look for the first user with provider: :facebook & uid: data.id,
#If now user is there go ahead and create one with first_or_create.
user = User.where(:provider => 'facebook', :uid => data.id).first_or_create do |user|
user.first_name = data.first_name,
user.last_name = data.last_name,
user.email = data.email,
user.password = Devise.friendly_token[0,20],
user.provider = 'facebook',
user.uid = data.id,
user.terms_od_use = true
end
end
Controller
def sign_in_method #dummy method
user = User.find_for_facebook_oauth(access_token, signed_in_resource=nil)
if user.valid?
redirect success_url #any url where u want to redirect on success
else user.errors
redirect root_url, error: user.errors.full_messages.join(', ')
end
end

Resources