I am trying (desperately) to setup my Rails 4 app with Devise and Omniauth.
My current efforts are in trying to get this tutorial to work for me.
http://sourcey.com/rails-4-omniauth-using-devise-with-twitter-facebook-and-linkedin/
Currently, I have:
Gemfile:
gem 'devise', '3.4.1'
gem 'devise_zxcvbn'
gem 'omniauth'
gem 'omniauth-oauth2', '1.3.1'
gem 'omniauth-google-oauth2'
gem 'omniauth-facebook'
gem 'omniauth-twitter'
gem 'omniauth-linkedin-oauth2'
gem 'google-api-client', require: 'google/api_client'
# gem 'oath'
Key differences between my gem file and the tutorial are that the tutorial does not include omniauth-oauth2 gem . I have included this because it is a requirement for google (which the tutorial does not use).
Identity.rb:
class Identity < ActiveRecord::Base
belongs_to :user
validates_presence_of :uid, :provider
validates_uniqueness_of :uid, :scope => :provider
def self.find_for_oauth(auth)
find_or_create_by(uid: auth.uid, provider: auth.provider)
end
end
My devise.rb includes config.omniauth for each of the 4 strategy providers.
My config/environment files include email settings for mailer actions.
routes.rb:
devise_for :users, #class_name: 'FormUser',
:controllers => {
# :registrations => "users/registrations",
# :omniauth_callbacks => "users/authentications"
:omniauth_callbacks => 'users/omniauth_callbacks'
}
match '/users/:id/finish_signup' => 'users#finish_signup', via: [:get, :patch], :as => :finish_signup
Omniauth callbacks controller:
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def self.provides_callback_for(provider)
class_eval %Q{
def #{provider}
@user = User.find_for_oauth(env["omniauth.auth"])
if @user.persisted?
sign_in_and_redirect @user, event: :authentication
set_flash_message(:notice, :success, kind: "#{provider}".capitalize) if is_navigational_format?
else
session["devise.#{provider}_data"] = env["omniauth.auth"]
redirect_to new_user_registration_url
end
end
}
end
[:twitter, :facebook, :linkedin, :google_oauth2].each do |provider|
provides_callback_for provider
end
def after_sign_in_path_for(resource)
if resource.email_verified?
super resource
else
finish_signup_path(resource)
end
end
end
Key differences between tutorial and my code: i have included google strategy
User.rb
class User < ActiveRecord::Base
TEMP_EMAIL_PREFIX = 'change@me'
TEMP_EMAIL_REGEX = /\Achange@me/
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable,
:confirmable, :lockable,
# :zxcvbnable,
:omniauthable, :omniauth_providers => [:facebook, :linkedin, :twitter, :google_oauth2 ]
mount_uploader :image, AvatarUploader
# --------------- associations
has_many :articles
has_many :authentications, :dependent => :delete_all
has_many :comments
belongs_to :organisation
has_one :profile
has_many :qualifications
has_many :identities
has_many :trl_assessments, as: :addressable
# has_and_belongs_to_many :projects
# --------------- scopes
# --------------- validations
# validates_presence_of :first_name, :last_name
validates_uniqueness_of :email
# per sourcey tutorial - how do i confirm email registrations are unique?
# this is generating an error about the options in the without function -- cant figure out the solution
validates_format_of :email, :without => TEMP_EMAIL_REGEX, on: :update
# --------------- class methods
# sourcey tutorial
def self.find_for_oauth(auth, signed_in_resource = nil)
# Get the identity and user if they exist
identity = Identity.find_for_oauth(auth)
# If a signed_in_resource is provided it always overrides the existing user
# to prevent the identity being locked with accidentally created accounts.
# Note that this may leave zombie accounts (with no associated identity) which
# can be cleaned up at a later date.
user = signed_in_resource ? signed_in_resource : identity.user
# p '11111'
# Create the user if needed
if user.nil?
# p 22222
# Get the existing user by email if the provider gives us a verified email.
# If no verified email was provided we assign a temporary email and ask the
# user to verify it on the next step via UsersController.finish_signup
email_is_verified = auth.info.email && (auth.info.verified || auth.info.verified_email)
email = auth.info.email if email_is_verified
user = User.where(:email => email).first if email
# Create the user if it's a new registration
if user.nil?
# p 33333
user = User.new(
# at least one problem with this is that each provider uses different terms to desribe first name/last name/email. See notes on linkedin above
first_name: auth.info.first_name,
last_name: auth.info.last_name,
email: email,
#username: auth.info.nickname || auth.uid,
password: Devise.friendly_token[0,20])
#
# debugger
if email_is_verified
user.skip_confirmation!
end
# user.skip_confirmation!
user.save!
end
end
# Associate the identity with the user if needed
if identity.user != user
identity.user = user
identity.save!
end
user
end
def email_verified?
self.email && TEMP_EMAIL_REGEX =~ self.email
end
def full_name
[*first_name.capitalize, last_name.capitalize].join(" ")
end
def formal_name
[*self.profile.try(:title), first_name.capitalize, last_name.capitalize].join(" ")
end
#----------- user to be approved by admin (for now); later approved by approver
def disapprove
self.approved = false
end
def approved
self.approved = true
end
def active_for_authentication?
super && approved?
end
def inactive_message
if !approved?
:not_approved
else
super # Use whatever other message
end
end
def self.send_reset_password_instructions(attributes={})
recoverable = find_or_initialize_with_errors(reset_password_keys, attributes, :not_found)
if !recoverable.approved?
recoverable.errors[:base] << I18n.t("devise.failure.not_approved")
elsif recoverable.persisted?
recoverable.send_reset_password_instructions
end
recoverable
end
# --------------- callbacks
# after_create :add_default_role
# --------------- instance methods
# --------------- private methods
# def add_default_role
# add_role(:pending) if self.roles.blank?
# end
end
Key differences between my code and tutorial:
changed name to split out between first and last name (to fit my schema) so used auth.info instead of raw info.
In the tutorial, the find_for_oauth method creates a new user with an email attribute that has a question mark and an alternative to email? I deleted the question mark and everything to the right of it because I couldn't get past an error in this line & i couldn't understand what it was trying to do.
I tried to handle confirmation exception with an if email_is_verified method instead of just skipping the confirmation.
I changed the email_verified method to:
self.email && TEMP_EMAIL_REGEX =~ self.email
I don't know why this change has been made -I tried to get help on codementor.io with these problems and this was one of the changes made (without explanation as to why).
- I have an extra validation to check the uniqueness of an email address.
Users controller:
class UsersController < ApplicationController
before_action :set_user, only: [:index, :show, :edit, :update, :destroy]
def index
if params[:approved] == "false"
@users = User.find_all_by_approved(false)
else
@users = User.all
end
end
# GET /users/:id.:format
def show
# authorize! :read, @user
end
# GET /users/:id/edit
def edit
# authorize! :update, @user
end
# PATCH/PUT /users/:id.:format
def update
# authorize! :update, @user
respond_to do |format|
if @user.update(user_params)
sign_in(@user == current_user ? @user : current_user, :bypass => true)
format.html { redirect_to @user, notice: 'Your profile was successfully updated.' }
format.json { head :no_content }
else
format.html { render action: 'edit' }
format.json { render json: @user.errors, status: :unprocessable_entity }
end
end
end
# GET/PATCH /users/:id/finish_signup
def finish_signup
# authorize! :update, @user
if request.patch? && params[:user] #&& params[:user][:email]
if @user.update(user_params)
@user.skip_reconfirmation!
sign_in(@user, :bypass => true)
redirect_to @user, notice: 'Your profile was successfully updated.'
else
@show_errors = true
end
end
end
# DELETE /users/:id.:format
def destroy
# authorize! :delete, @user
@user.destroy
respond_to do |format|
format.html { redirect_to root_url }
format.json { head :no_content }
end
end
private
def set_user
@user = User.find(params[:id])
end
def user_params
params.require(:user).permit(policy(@user).permitted_attributes)
# accessible = [ :first_name, :last_name, :email ] # extend with your own params
# accessible << [ :password, :password_confirmation ] unless params[:user][:password].blank?
# accessible << [:approved] if user.admin
# params.require(:user).permit(accessible)
end
end
Key changes from tutorial: strong params are now defined in pundit user policy (exactly as per commented out bit above) although I have tried solving this problem with the strong params in the controller. It makes no difference to the current problem.
Finish sign up form:
<%= form_for(current_user, :as => 'user', :url =>
finish_signup_path(current_user), :html => { role: 'form'}) do |f| %>
<% if @show_errors && current_user.errors.any? %>
<div id="error_explanation">
<% current_user.errors.full_messages.each do |msg| %>
<%= msg %><br>
<% end %>
</div>
<% end %>
<div class="form-group">
<!-- f.label :false -->
<div class="controls">
<%= f.text_field :email, :autofocus => true, :value => '', class: 'form-control input-lg', placeholder: 'Example: [email protected] -- use your primary work address' %>
<p class="help-block">Please confirm your email address. No spam.</p>
</div>
</div>
<div class="actions">
<%= f.submit 'Continue', :class => 'btn btn-primary' %>
Currently, when I try to register with twitter, I get this error message:
ActiveRecord::StatementInvalid in Users::OmniauthCallbacksController#twitter
PG::NotNullViolation: ERROR: null value in column "email" violates not-null constraint DETAIL: Failing row contains (19, null, null, null, $2a$10$aye71.1oEClA5vsy..8WzeknYl9wxi7W8VDEGlNbzakQQyJqe8i8q, null, null, null, 0, null, null, null, null,
4e25979ab053eb4a26a6ba451d9, null, 2015-12-14 01:00:08.431268, null, 0, null, null, 2015-12-14 01:00:08.428961, 2015-12-14 01:00:08.428961, null, f). : INSERT INTO "users" ("email", "encrypted_password", "created_at", "updated_at", "confirmation_token", "confirmation_sent_at") VALUES ($1, $2, $3, $4, $5, $6) RETURNING "id"
I don't know what this error means, or why I'm getting it. I think it most likely has something to do with the differences between my code and the tutorial outlined above, but I can't see which of them is causing the problem or how to go about solving it.
When I try the same thing with linkedin, I get this error:
NoMethodError (undefined method `approved?' for #<User:0x007fa90c867600>):
2015-12-14T01:02:10.472783+00:00 app[web.1]: app/models/user.rb:130:in `active_for_authentication?'
I have an approved attribute in my schema. I also added a method called 'approved' to my user model to try and solve this, but it isn't working.
Can anyone see anything that might help. I've been trying to get this set up for the best part of 3 years. I've dumped $$$$ into code mentors, paid various consultants and been to every rails meetup in my area, but I can't find anyone who can help.
Taking Moustafa's suggestions, in relation to the linkedin problem:
I commented out each of these approval methods from my user.rb:
# def disapprove
# self.approved = false
# end
# def approved?
# self.approved = true
# end
# def active_for_authentication?
# super && approved?
# end
# def inactive_message
# if !approved?
# :not_approved
# else
# super # Use whatever other message
# end
# end
The first two are there in an attempt to solve this approved? problem and the second two are there because, following this tutorial, I want to allow an admin to approve a newly registered user before they can sign in.
Anyway, commenting those out and trying again, I then get this error:
Started PATCH "/users/1/finish_signup" for 49.191.133.120 at 2015-12-14 08:18:49 +0000
2015-12-14T08:18:49.536922+00:00 app[web.1]:
2015-12-14T08:18:49.536927+00:00 app[web.1]: NameError (undefined local variable or method `user' for #<UsersController:0x007f7215732e48>):
2015-12-14T08:18:49.536929+00:00 app[web.1]: app/controllers/users_controller.rb:72:in `user_params'
2015-12-14T08:18:49.536930+00:00 app[web.1]: app/controllers/users_controller.rb:43:in `finish_signup'
2015-12-14T08:18:49.536941+00:00 app[web.1]:
Line 72 of my users controller has this:
accessible << [:approved] if user.admin
ATTEMPT 2:
I uncommented the last two approved methods (being those suggested in the devise tutorial for admin approval of newly registered users). When I save and restart the server, the home page won't even load. The logs say:
Completed 500 Internal Server Error in 21ms (ActiveRecord: 1.8ms)
2015-12-14T08:45:09.932008+00:00 app[web.1]:
2015-12-14T08:45:09.932010+00:00 app[web.1]: ActionView::Template::Error (undefined method `approved?' for #<User:0x007f6afe8a2878>):
2015-12-14T08:45:09.932012+00:00 app[web.1]: 12: <span class="deviselinks" style="padding-right:30px">
2015-12-14T08:45:09.932011+00:00 app[web.1]: 10: <div class="col-md-8>">
2015-12-14T08:45:09.932012+00:00 app[web.1]: 11: <ul style="text-align: right">
2015-12-14T08:45:09.932016+00:00 app[web.1]: app/models/user.rb:130:in `active_for_authentication?'
2015-12-14T08:45:09.932014+00:00 app[web.1]: 14: Hi <%= link_to(current_user.first_name.titlecase, profile_path(current_user)) %></span>
2015-12-14T08:45:09.943612+00:00 heroku[router]: at=info method=GET path="/" host=www.[].com request_id=a5646532-14a6-46ed-8dd7-6e4741688cf3 fwd="49.191.133.120" dyno=web.1 connect=1ms service=34ms status=500 bytes=1754
I don't know what full stack trace means - is that a terminal command?