Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails 4 - Devise Omniauth (multiple strategies)

I'm trying to make an app in Rails 4. I have been trying for the last 3 years (except for 10 days), to get devise to work.

I'm trying to follow this tutorial: http://sourcey.com/rails-4-omniauth-using-devise-with-twitter-facebook-and-linkedin/

Please don't recommend other tutorials / gem documentation. I have tried at least 30 other tutorials and the gem documentation is full of errors and components that I don't understand.

My current problem is that when I get to the finish sign up step in this tutorial, the form asks me for my email address.

The users controller has a finish signup method as:

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 root_path, notice: 'Your profile was successfully updated.'
        # redirect_to [@user, @user.profile || @user.build_profile]
        sign_in_and_redirect(@user, :bypass => true)
      else
        @show_errors = true
      end
    end
  end

When I try this, I get this error:

undefined method `match' for {:host=>"localhost", :port=>3000}:Hash

The error points at this line:

        <div class="intpol3"><%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></div>

My development environment is set up to include all the config details for my email sender.

When I try the same step in production mode, I get this error:

ActionView::Template::Error (No route matches {:action=>"show", :controller=>"profiles", :id=>nil} missing required keys: [:id]):

It's looking for a profile id because I have an after_create action in my user model as:

after_create :gen_profile

  def gen_profile
    Profile.create(user: self) # Associations must be defined correctly for this syntax, avoids using ID's directly.
    # Profile.save
  end

My other issue with this tutorial is that the fields in the identity table aren't being populated.

I'd love to find someone that has successfully implemented this tutorial or can see how to make this work.

My code is:

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'

routes

devise_for :users, #class_name: 'FormUser',
             :controllers => {
                :registrations => "users/registrations",
                # :omniauth_callbacks => "users/authentications"
                :omniauth_callbacks => 'users/omniauth_callbacks'
           }

  # get '/auth/:provider/callback' => 'users/authentications#create'
  # get '/authentications/sign_out', :to => 'users/authentications#destroy' 

  # PER SOURCEY TUTORIAL ----------
  match '/users/:id/finish_signup' => 'users#finish_signup', via: [:get, :patch], :as => :finish_signup

resources :users do
     resources :profiles, only: [:new, :create]
  end

user.rb

class User < ActiveRecord::Base

  TEMP_EMAIL_PREFIX = 'change@me'
  TEMP_EMAIL_REGEX = /\Achange@me/

  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable,
          :confirmable, :lockable,
         # :zxcvbnable,
         :omniauthable, :omniauth_providers => [:facebook, :linkedin, :twitter, :google_oauth2 ]





  # --------------- associations

  has_many :authentications, :dependent => :delete_all

  has_one :profile

  has_many :identities


  # --------------- 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 # take out this if stmt for chin yi's solution
      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 ? email : "#{TEMP_EMAIL_PREFIX}-#{auth.uid}-#{auth.provider}.com",
          #username: auth.info.nickname || auth.uid,
          password: Devise.friendly_token[0,20])
# fallback for name fields - add nickname to user table
        # 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

users controller

class UsersController < ApplicationController

before_action :set_user, only: [:index, :show, :edit, :update, :finish_signup, :destroy]

# i added finish_signup to the set_user action (not shown in tutorial)

  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 root_path, notice: 'Your profile was successfully updated.'
        # redirect_to [@user, @user.profile || @user.build_profile]
        sign_in_and_redirect(@user, :bypass => true)
      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, :avatar ] # 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

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"], current_user) 

        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

  # , current_user has been deleted from the end of line 51
  #come back to put current_user into fidn by oauth so i can link other accounts - i have added this back for the purpose of solving the current problem

          # puts current_user.inspect
           # sign_in_and_redirect [@user, @user.profile || @user.build_profile]

          # sign_in_and_redirect_user(:user, event: :authentication)


  [: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

registrations controller

class Users::RegistrationsController < Devise::RegistrationsController 




      protected

      def after_sign_up_path_for(resource)
      profile_path(resource)
  end


  private
    def user_params
          params.require(:user).permit(:first_name, :last_name, :email, :password )
    end

    end

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

Identities controller

class IdentitiesController < ApplicationController
  before_action :set_identity, only: [:show, :edit, :update, :destroy]
  before_action :authenticate_user!

  # GET /identities
  # GET /identities.json
  def index
    @identities = Identity.all
  end

  # GET /identities/1
  # GET /identities/1.json
  def show
  end

  # GET /identities/new
  def new
    @identity = Identity.new
  end

  # GET /identities/1/edit
  def edit
  end

  # POST /identities
  # POST /identities.json
  def create
    @identity = Identity.new(identity_params)

    respond_to do |format|
      if @identity.save
        format.html { redirect_to @identity, notice: 'Identity was successfully created.' }
        format.json { render :show, status: :created, location: @identity }
      else
        format.html { render :new }
        format.json { render json: @identity.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /identities/1
  # PATCH/PUT /identities/1.json
  def update
    respond_to do |format|
      if @identity.update(identity_params)
        format.html { redirect_to @identity, notice: 'Identity was successfully updated.' }
        format.json { render :show, status: :ok, location: @identity }
      else
        format.html { render :edit }
        format.json { render json: @identity.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /identities/1
  # DELETE /identities/1.json
  def destroy
    @identity.destroy
    respond_to do |format|
      format.html { redirect_to identities_url, notice: 'Identity was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_identity
      @identity = Identity.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def identity_params
      params[:identity]
    end
end

devise mailer - confirmation

        <div class="intpol3"><%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></div>

SUMMARY OF CURRENT PROBLEMS:

  1. In development mode: There is a problem with the link to the confirmation token. I can't find any materials that indicate why this might arise. The error is:(ActionView::Template::Error (undefined method `match' for {:host=>"localhost", :port=>3000}:Hash):

  2. In production mode, there is an error with user looking for profile id. The error message is: ActionView::Template::Error (No route matches {:action=>"show", :controller=>"profiles", :id=>nil} missing required keys: [:id]):

My profiles routes are:

resources :profiles, only: [:show, :edit, :update, :destroy]
resources :users do
     resources :profiles, only: [:new, :create]
  end
  1. None of the fields in the identity model are populating. They are all showing as nil.

THINGS DONE DIFFERENTLY THAN AS SHOWN IN THE TUTORIAL:

  1. I also allow email sign up

  2. I add 'finish_sign_up' to the set_user before action in the users controller

  3. I add g+ strategy (which means my gems are slightly different)

  4. My new user method does not use raw info. It uses oauth processed info.

  5. My redirect in the finish sign up method is slightly different, although I've commented that out and gone back to the way it is set out in the tutorial to try to get this working (although the above problems are repeating).

I'm going crazy trying to solve these problems. I'd say 3 years is way too long to be stuck on this problem. If anyone can help, I'd pay it forward 10x and then some. Thank you.

like image 774
Mel Avatar asked Jan 08 '16 09:01

Mel


1 Answers

<div class="intpol3"><%= link_to 'Confirm my account', 
  confirmation_url(@resource, confirmation_token: @token) %></div>

Try resource instead of @resource. AFAIK it's only a helper_method, NOT an instance variable.

I think that will solve your problem fully in production. Since @resource is not the same as resource, it hasn't been set, and you're basically calling confirmation_url(nil, confirmation_token: @token), and that nil is getting passed through to the error message.

In development, there appears to be an additional issue, which most probably has to do with how you've configured config.action_mailer.default_url_options in config/environments/development.rb and most likely is raising the exception in ActionDispatch::Http::Url.build_host_url. I suspect you have something like:

config.action_mailer.default_url_options[:host] = { host: 'localhost', port: 9000 }

Change that to:

config.action_mailer.default_url_options[:host] = 'localhost:9000'

And see if that solves everything. If I'm wrong about how config.action_mailer.default_url_options is configured, please paste your config/environments/development.rb AND a full stack trace from your development error so we can help you further.

like image 73
Isaac Betesh Avatar answered Oct 12 '22 23:10

Isaac Betesh