Hello everyone and thank you in advanced for your expertise and help.
I'm not very skilled in Ruby on Rails (started merely 3 months ago) and am building a rather complex application. I have hit a roadblock concerning Omniauth-Facebook. I have Devise, omniauth-facebook, an omniauth_callbacks Controller, a Devise registrations_controller, a Devise users_controller, and a User Model with validations for a multitude of fields (see all code below). When setting this all up, I more or less followed this Youtube video: https://www.youtube.com/watch?v=VeUX3pWn28w and many other scattered guides when I attempted troubleshooting. Some were outdated and I'm not even sure anymore what parts of my code could be outdated or not.
What's working from my Signup form: A user can fill out my custom signup form and be properly added to the database with all Devise actions functioning correctly and all custom table attributes being recorded. A user can click the "Signup with Facebook" button and be directed to Facebook to login/signup, where Provider and UID is being returned, as well as (it seems) First Name, Email, and Password. I have State and Zip defaulted in my schema, so that doesn't raise an validation exception.
What seems to be failing: When Facebook is sending back the info, it seems to break at Email returning ("User Exists"). Even though I've included "auth.info.last_name, auth.info.image, etc.", the terminal error never shows those additional fields returning from Facebook, but the validations don't throw an exception, including email. In my database, I made sure there is no duplication of that email. Due to my DB and validations, it seems my required Last Name, Gender, DOB, Address and City are not being filled in as well, which is understandable (besides last_name) because I have no way of receiving that from Facebook.
This is what I want: As I mentioned above, Facebook doesn't seem to be returning "last_name", "address" or "image". I also need to include Birth Date, Gender and their City. It seems I cannot do that from omniauth-facebook since Facebook's Auth Hash doesn't include those fields. How can I allow a user to sign up via Facebook with those provided fields, be signed in via Facebook, but then be directed to a page where they can enter the additional information that's missing that my DB requires? So after signing up via facebook, how do I make it so a User is sent to another page (without having those validation exceptions being thrown up?) to then enter that additional information (last name, date of birth, address, address2 [if needed], gender, and city/state/zip, or/else any other fields missing)? And why might "last_name" be failing, but "first_name" is fine?
Below is a picture of everything you guys might need and I think may be required. Again, I understand the basics of Ruby and Rails and would consider myself intermediate, but need help in understanding why/how the answer fixes this issue I am experiencing. I sadly lack knowledge on how advanced Routes and Controllers work so if you have any resource that'll help me sharpen my knowledge of that subject, I'd appreciate that, too.
Users Table (schema.rb):
create_table "users", force: :cascade do |t|
t.string "first_name", null: false
t.string "last_name", null: false
t.boolean "admin", default: false
t.boolean "subscribed", default: true
t.string "zip", default: "44107", null: false
t.string "phone_number"
t.string "address", null: false
t.string "city_name", default: "Lakewood", null: false
t.string "state", default: "Ohio", null: false
t.date "dob", null: false
t.integer "city_id"
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.string "confirmation_token"
t.datetime "confirmed_at"
t.datetime "confirmation_sent_at"
t.string "unconfirmed_email"
t.integer "failed_attempts", default: 0, null: false
t.string "unlock_token"
t.datetime "locked_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "address2"
t.boolean "gender"
t.string "provider"
t.string "uid"
t.string "name"
t.text "image"
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true
end
Users Model (user.rb):
class User < ApplicationRecord
# Below - Devise Modules. We have not used = :timeoutable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable,
:validatable, :confirmable,
:omniauthable, omniauth_providers: [:facebook]
# Below - Additional validations of DB field entries presence at Model-level.
validates :first_name, presence: true, length: { maximum: 30 }
validates :last_name, presence: true, length: { maximum: 30 }
validates :dob, presence: { message: "(Date of Birth) must be entered" }
validates :zip, presence: true, length: { maximum: 11 }
validates :city_name, presence: true
validates :state, presence: true, length: { maximum: 15 }
validates :address, presence: true
validates :gender, presence: { message: "must be selected" }
# Below - Associates Users into a One to Many relationship with Cities.
belongs_to :city
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.from_omniauth(auth)
where(provider: auth.provider, uid: auth.uid).first_or_create! do |user|
user.provider = auth.provider
user.uid = auth.uid
user.email = auth.info.email
user.first_name = auth.info.first_name
user.password = Devise.friendly_token[0,20]
user.last_name = auth.info.last_name
user.image = auth.info.image
user.address = auth.info.location
user.skip_confirmation!
user.save
end
end
# Below - Turns emails into downcase when saved from the controller into DB.
before_save { self.email = email.downcase }
end
Sessions Controller (controllers/users/sessions_controller.rb):
class Users::SessionsController < Devise::SessionsController
# before_action :configure_sign_in_params, only: [:create]
class SessionsController < ApplicationController
def create
@user = User.find_or_create_from_auth_hash(auth_hash)
self.current_user = @user
redirect_to '/'
end
protected
def auth_hash
request.env['omniauth.auth']
end
end
Registrations Controller (controllers/users/registrations_controller.rb):
class Users::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create]
# before_action :configure_account_update_params, only: [:update]
# GET /resource/sign_up
def new
@user = User.new
end
# POST /resource
def create
super
end
# GET /resource/edit
def edit
super
end
# PUT /resource
def update
super
end
protected
# If you have extra params to permit, append them to the sanitizer.
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name, :gender, :dob, :admin, :phone_number, :address, :address2, :city_name, :state, :zip, :subscribed, :city_id, :name, :image, :uid, :provider])
end
end
Users Controller (controllers/users_controller.rb):
class UsersController < ApplicationController
# Main Users Controller
# Index Action for all Users
def index
@users = User.all
end
# Show Action for individual user ID
def show
@user = User.find(params[:id])
end
end
Omniauth Callback Controller (controllers/user/omniauth_callbacks_controller.rb):
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
# Main MOdel for Facebook OmniAuth User Login/Signup
def facebook
#raise request.env["omniauth.auth"].to_yaml
@user = User.from_omniauth(request.env["omniauth.auth"])
if @user.persisted?
sign_in_and_redirect @user, :event => :authentication
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
def failure
redirect_to root_path
end
end
Application Controller (controllers/application_controller.rb):
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
before_action :configure_permitted_parameters, if: :devise_controller?
# Below, permits strong params from signup process, sign_in, and Account_update
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:login, keys: [:email, :password])
devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name, :address, :address2, :dob, :city_name, :state, :zip, :gender, :phone_number, :subscribed, :email, :password, :password_confirmation, :city_id, :name, :image, :uid, :provider])
devise_parameter_sanitizer.permit(:account_update, keys: [:email, :address, :address2, :city_name, :state, :zip, :phone_number, :subscribed, :password, :password_confirmation, :current_password])
end
end
Devise Initializer (config/initializers/devise.rb):
# ==> OmniAuth
# Add a new OmniAuth provider. Check the wiki for more information on setting
# up on your models and hooks.
config.omniauth :facebook, 'BLANK', 'BLANK', callback_url: 'MYWEBADRESS/users/auth/facebook/callback'
Routes (routes.rb):
Rails.application.routes.draw do
# Main Routes file for all route, URL names and Action Methods.
# Below - Sets up custom login and log out path names for routes for user model.
devise_for :users, path_names: { sign_in: "login", sign_out: "logout" }, controllers: {
sessions: 'users/sessions',
:omniauth_callbacks => "users/omniauth_callbacks" }
# Not logged in visitors will be greeted by Index page
root 'visitors#index'
get 'cities/index'
# Callback Route after OmniAuth redirects back to Ossemble for User Signup
get '/auth/facebook/callback', to: 'sessions#create'
# Below - Sets up routes for admin model.
devise_for :admins
# Below - Creates all routes for City model.
resources :cities, except: [:destroy]
# Below - Creates show route for Users & Model
resources :users, only: [:show, :index]
end
And lastly my Signup Form (views/devise/registrations/new.html.erb):
<%= form_for(resource, as: resource_name, url: registration_path(resource_name),
:html => {class: "form", role: "form"}) do |f| %>
<div class="container alert"> <!-- Begin - Error Message(s) Wrapper for User Signup -->
<%= devise_error_messages! %>
</div> <!-- End - Error Message(s) Wrapper for User Signup -->
<div class="container"> <!-- Begin -- Signup Form -->
<div class="form-row"> <!-- Begin - First, Last Name, Gender, & Date of Birth Form Row -->
<div class="form-group col-md-4">
<div class="control-label">
<%= f.label :first_name, "First Name" %>
</div>
<%= f.text_field :first_name, autofocus: true, class: "form-control", placeholder: "Enter First Name" %>
</div>
<div class="form-group col-md-4">
<div class="control-label">
<%= f.label :last_name, "Last Name" %>
</div>
<%= f.text_field :last_name, class: "form-control", placeholder: "Enter Last Name" %>
</div>
<div class="form-group col-md-2">
<div class="control-label">
<label class="control-label">
Gender
</label>
</div>
<div class="gender_form form-control center" >
<%= f.label :gender, "Male", id: "male_text", class: "gender_text" %>
<%= f.radio_button :gender, true, class: "gender_box"%>
<%= f.label :gender, "Female", id: "female_text", class: "gender_text" %>
<%= f.radio_button :gender, false, class: "gender_box" %>
</div>
</div>
<div class="form-group col-md-2">
<div class="control-label">
<%= f.label :dob, "Birth Date" %>
</div>
<%= f.date_field :dob, class: "form-control" %>
</div>
</div>
<br> <!-- End - of Name, Gender & DOB form row -->
<!-- Begin - Email & Password Form Row -->
<div class="form-row">
<div class="form-group col-md-4">
<div class="control-label">
<%= f.label :email, "Email Address" %>
</div>
<%= f.email_field :email, type:"text", class: "form-control", placeholder: "Enter Email Address: youremail@example.com" %>
</div>
<div class="form-group col-md-4">
<div class="control-label">
<%= f.label :password, "Password" %>
<!-- Begin - If statement checks for password minimum character length -->
<small>
(Minimum: <%= @minimum_password_length %> characters)
</small>
</div>
<%= f.password_field :password, autocomplete: "off", id:"inputPassword", class: "form-control", placeholder: "Create a Password" %>
<!-- End - password character length if statement -->
</div>
<div class="form-group col-md-4">
<div class="control-label">
<%= f.label :password_confirmation, "Confirm Password" %>
</div>
<%= f.password_field :password_confirmation, autocomplete: "off", id:"inputPasswordConfirm", class: "form-control", placeholder: "Re-enter Password" %>
</div>
</div>
<br> <!-- End - Email & Password Form Row -->
<!-- Begin - Address & Phone Number Form Row-->
<div class="form-row">
<div class="form-group col-md-4">
<div class="control-label">
<%= f.label :address, "Primary Address" %>
</div>
<%= f.text_field :address, autocomplete: "on", class: "form-control", placeholder: "Enter Address: 1234 Main Street" %>
</div>
<div class="form-group col-md-4">
<div class="control-label">
<%= f.label :address2, "Secondary Address" %> <small> (optional) </small>
</div>
<%= f.text_field :address2, autocomplete: "on", class: "form-control", placeholder: "Apartment, Suite #" %>
</div>
<!-- Phonne number currently hideen on form due to "style-" and "disabled :true" -->
<div class="form-group col-md-3" style="display: none">
<div class="control-label">
<%= f.label :phone_number, "Phone Number" %>
</div>
<%= f.phone_field :phone_number, disabled: true, class: "form-control", placeholder: "Enter Phone #: 555-555-5555" %>
</div>
</div> <br> <!-- End - Address & Phone Number Row -->
<!-- Beginning - Location Form Row -->
<div class="form-row">
<div class="form-group col-md-4">
<div class="control-label">
<%= f.label :city_name, "City" %>
<small>
(Currently Lakewood, OH Supported)
</small>
</div>
<!-- Below, we list all cities to be selected and we set the user's "city_id" field to the corresponding City table's City ID, completing the User's Association to City -->
<%= f.collection_select :city_id, City.all, :id, :name, {prompt: "Choose a City"}, class: "select_form form-control" %>
</div>
<div class="form-group col-md-2 ">
<div class="control-label">
<%= f.label :state, "State" %>
<small>
(Currently Ohio)
</small>
</div>
<%= f.collection_select :state, City.all, :state, :state, {prompt: "Choose a State"}, class: "select_form form-control" %>
</div>
<div class="form-group col-md-2">
<div class="control-label">
<%= f.label :zip, "Zip Code" %> <small> (5-Digit Zip)</small>
</div>
<%= f.collection_select :zip, City.all, :zip, :zip, {prompt: "Choose Zip-Code"}, maxlength: "5", class: "select_form form-control" %>
</div>
</div> <!-- End - Location Form Row -->
<!-- Begin - Subscribe Option -->
<div class="form-row">
<div class="form-group col-md-12 pull-left">
<div class="form-check">
<div class="control-label">
<%= f.label :subscribed, "Subscribe?" %>
<small>
<em>
Stay up to date with Ossemble!
</em>
</small>
<br>
<div class="form-row pull-left">
<div class="col-md-12">
<div>
<%= f.check_box :subscribed, class: "check_box" %>
<%= f.label "Yes!", class: "check_box_label" %>
</div>
</div>
</div>
</div>
</div>
</div>
</div> <!-- End - Subscribe Option -->
<div class="form-row"> <!-- Begin - Create Account Button Row (FB) -->
<div class="form-group col-md-2">
<div class="actions">
<%= f.submit "Create Account", class: "form_btn btn btn-success btn-lg btn-xlarge" %>
</div>
</div>
<div class="form-group col-md-4"> <!-- Begin - Signup With Facebook Button -->
<%- if devise_mapping.omniauthable? %> <!-- Begin - OmniAuth If Statement -->
<!-- Below - OmniAuth & FB Signup Button -->
**<%= link_to '<i class="fa fa-facebook fa-lg fa-fw fb_log_sign_icon" aria-hidden="true"></i> Facebook Signup'.html_safe,
user_facebook_omniauth_authorize_path,
class: "form_btn btn btn-primary btn-lg btn-xlarge" %> <br />
<% end -%>** <!-- End - OmniAuth If Statement -->
</div> <!-- End - Signup with Facebook Button -->
</div> <!-- End - Create Account Button Row (FB) -->
</div> <!-- End - Signup Form -->
<% end %>
In your Devise initializer (config/initializers/devise.rb
) you need to use the info_fields
parameter to add the items you want Facebook to return. Facebook only returns name, email and id by default. For example:
# Add a new OmniAuth provider. Check the wiki for more information on setting
# up on your models and hooks.
config.omniauth :facebook, ENV['FACEBOOK_APP_ID'], ENV['FACEBOOK_APP_SECRET'],
callback_url: 'https://ossemble-workspace-twistedben.c9users.io/users/auth/facebook/callback',
info_fields: 'email,name,birthday,locale,gender'
You can get a list of the items available on the Facebook user object here. Some of the items (such as birthday) require the permission of the user and special approval of your app by Facebook. You'll need to thoroughly explore this in the documentation.
Also, a Facebook user can deny your app access to any part of his/her profile. Just because your request the email from Facebook, does not guarantee the call back will include the email. Your app will need to gracefully handle those cases.
In your case, you may want to create a new user record but not persist it right away. Instead, pass that object to a registration form that lets the user fill in the blanks in his/her profile. Or you can save the new record without validation (user.save(validate:false)
).
Also, I recommend storing your app id and app secret in an environment variable (ENV[...]
) and not checking it into source code or posting it online. You will want to get a new Facebook secret immediately.