Step 10: update /models/social_account.rb model to the following
class SocialAccount < ApplicationRecord
AVAILABLE_PROVIDERS = %w(twitter facebook linkedin instagram)
belongs_to :user
validates :provider, presence: true, inclusion: { in: AVAILABLE_PROVIDERS }
validates :uid, uniqueness: { scope: :provider }
class << self
def from_omniauth(auth, user)
# We try to fetch social_account based on the passed provider and uid
# If such social account cannot be found, we define (initialize) a new one
# Please note that initialization means that we ONLY store it in memory - we do not save it to the
# database yet
social_account = find_or_initialize_by(provider: auth['provider'], uid: auth['uid'])
unless social_account.user
# if the social_account does not have any parent user
# we check if the user was passed
# if no user is set, we raise an error - it means that we simply don't have enough data about the user
# if the user IS set, we store its id
user.nil? ? raise(RecordNotFound) : social_account.user = user
end
# now just save any other data
social_account.name = auth['info']['name']
# save the social_account into the database
social_account.save!
# and return it as a result
social_account
end
end
end
Step 11: add logic to models/user.rb model
class User < ApplicationRecord
has_secure_password
has_many :social_accounts, dependent: :destroy
validates :email, presence: true, uniqueness: true
def has_provider?(provider)
!with_provider(provider).nil?
end
# Fetch user's social account by provider's name
def with_provider(provider)
social_accounts.find_by(provider: provider)
end
# def method_missing(name, *args, &block)
# provider = name.match(/has_(\w+)\?\z/)
# if provider && SocialAccount::AVAILABLE_PROVIDERS.include?(provider[1])
# social_accounts.where(provider: provider).any?
# else
# super
# end
# end
end
Step 12: add logic to users_controller.rb
class UsersController < ApplicationController
skip_before_action :authenticate_user!, only: [:new, :create]
before_action :require_no_authentication, only: [:new, :create]
# Users cannot modify profiles they do not own
before_action :check_owner!, only: [:edit, :update]
def edit
@providers = SocialAccount::AVAILABLE_PROVIDERS
end
def update
end
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
flash[:success] = "Welcome!"
sign_in @user
redirect_to root_path
else
render :new
end
end
private
def user_params
# :password and :password_confirmation are virtual attributes - they are not being
# stored to the database. Instead we store a hash of the password
# Read more here
# http://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html#method-i-has_secure_password
params.require(:user).permit(:email, :password, :password_confirmation)
end
def check_owner!
@user = User.find_by(id: params[:id])
redirect_to root_path unless @user && current_user == @user
end
end
Step 13: add logic to social_accounts_controller.rb
class SocialAccountsController < ApplicationController
skip_before_action :authenticate_user!, except: [:destroy]
# We have to check the owner. If the requested account does not belong to the
# currently logged in user, do nothing.
before_action :check_owner!, only: [:destroy]
def create
begin
# go ahead and try to create a new social account or find an existing one
# note that current_user may return nil
@social_account = SocialAccount.from_omniauth(request.env['omniauth.auth'], current_user)
# We rescue from the error that is raised when we do not have enough information about the user
rescue RecordNotFound
# Store the authentication hash in the session - we will require it a bit later
session[:auth_hash] = request.env['omniauth.auth'].except('extra', 'credentials')
flash[:success] = "Please provide some more data..."
# Redirect to another page with a form to provide additional data
redirect_to additional_info_social_accounts_path and return
end
# Otherwise we take the associated user and sign him in (unless he is already signed in)
sign_in(@social_account.user) unless current_user
redirect_to edit_user_path(@social_account.user)
end
def additional_info
# If authentication hash is not set, we just redirect to the root page
# We require this hash to proceed
redirect_to root_path unless session[:auth_hash]
# This hash will be used to build a form
@auth_hash = session[:auth_hash]
end
def finalize
# Fetch the provided e-mail and find or set a new user
@user = User.find_or_initialize_by(email: params[:email])
# If the user was not found in the database, it will be marked as a new record (meaning that it is not yet saved)
if !@user.new_record?
# If the user WAS found, we have to check that the correct password was entered
# This is done to prevent malicious users from logging in via accounts they do not own
redirect_to root_path and return unless @user.authenticate(params[:password])
else
# If the user was not found, we have to set a password for him
@user.password = params[:password]
@user.password_confirmation = params[:password]
# If @user.save returned false, it means that some incorrect data were provided
render 'additional_info' and return unless @user.save
end
# Now just try to create social_account again - this time it should be successful, because we do
# have a user account now
@social_account = SocialAccount.from_omniauth(session.delete(:auth_hash), @user)
# Lastly, sign him in
sign_in @social_account.user
redirect_to edit_user_path(@social_account.user)
end
def destroy
@social_account.destroy
redirect_to edit_user_path(@social_account.user)
end
private
def check_owner!
@social_account = SocialAccount.find_by(id: params[:id])
redirect_to root_path unless @social_account && current_user.social_accounts.include?(@social_account)
end
end
Step 14: add logic to sessions_controller.rb
class SessionsController < ApplicationController
skip_before_action :authenticate_user!, except: [:destroy]
before_action :require_no_authentication, only: [:new, :create]
def new
@providers = SocialAccount::AVAILABLE_PROVIDERS
end
def create
@user = User.find_by(email: params[:email])
# authenticate is a method provided by bcrypt gem; it returns either true or false
if @user && @user.authenticate(params[:password])
sign_in @user
flash[:success] = "Welcome"
redirect_to root_path
else
# This flash message should be visible only once, when we render the page, not after the reloading
# of the page
flash.now[:exceptions] = "Login and/or password is incorrect."
render 'new'
end
end
def destroy
sign_out
flash[:success] = "See you!"
redirect_to new_session_path
end
end
Step 15: add logic to application_controller.rb
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
before_action :authenticate_user!
private
def authenticate_user!
# user has to be authenticated
redirect_to new_session_path unless user_signed_in?
end
def require_no_authentication
# user has to be NOT authenticated
redirect_to root_path if current_user
end
def sign_in(user)
session[:user_id] = user.id
@current_user = user
end
def user_signed_in?
!current_user.nil?
end
def current_user
# We either return a previously set @current_user variable or
# assign value now
session[:user_id] ? @current_user ||= User.find_by(id: session[:user_id]) : nil
end
def sign_out
session.delete(:user_id)
@current_user = nil
end
# These methods should be available in the views as well
helper_method :current_user, :user_signed_in?
end
Step 16: modify views/application.html.erb to look as follows
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
resources :users, only: [:new, :create, :edit, :update]
resources :sessions, only: [:new, :create]
resources :social_accounts, only: [:destroy] do
collection do
get 'additional_info'
post 'finalize'
end
end
delete '/logout', to: 'sessions#destroy', as: :logout
get '/auth/:provider/callback', to: 'social_accounts#create'
root 'static_pages#index'
get 'static_pages/about'
get 'static_pages/faq'
get 'static_pages/contact'
end
Step 24: startup rails server
this app isn't complete
About
Multi Social Login; Twitter, Facebook, Linkedin, Instagram and username/psw login, No Devise