I am unable to override the Rails serializer when using devise_token_auth and active_model_serializer for Devise sign_up method.
I would like to customize the returned fields from the Devise sign_up controller when querying my API.
The devise_token_auth gem documentation indicates:
To customize json rendering, implement the following protected controller methods
Registration Controller
...
render_create_success
...
Note: Controller overrides must implement the expected actions of the controllers that they replace.
That is all well and good, but how do I do this?
I've tried generating a UserController serializer like the following:
class UsersController < ApplicationController
def default_serializer_options
{ serializer: UserSerializer }
end
# GET /users
def index
@users = User.all
render json: @users
end
end
but it's only being used for custom methods such as the index method above: it's not being picked up by devise methods like sign_up
I would appreciate a detailed response since I've looked everywhere but I only get a piece of the puzzle at a time.
Devise sign_up corresponds to devise_token_auth registrations controller and Devise sign_in corresponds to devise_token_auth sessions controller. Therefore when using this gem, customizing Devise sign_in and sign_up methods requires customizing both of these devise_token_auth controllers.
There are two ways to go about this based on what you need to accomplish.
Method #1
If you want to completely customize a method in the controller then follow the documentation for overriding devise_token_auth controller methods here: https://github.com/lynndylanhurley/devise_token_auth#custom-controller-overrides
This is what I did and it's working fine:
#config/routes.rb
...
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
sessions: 'overrides/sessions',
registrations: 'overrides/registrations'
}
...
This will route all devise_token_auth sessions and registrations to LOCAL versions of the controllers if a method exists in your local controller override. If the method does not exist in your local override, then it will run the method from the gem. You basically have to copy the controllers from the gem into 'app/controllers/overrides' and make any changes to any method you need to customize. Erase the methods from the local copy you are not customizing. You can also add callbacks in this way. If you want to modify the response, customize the the render at the end of the method that will return the response as json via active_model_serializer.
This is an example of my sessions controller which adds a couple of custom before_actions to add custom functionality:
#app/controllers/overrides/sessions_controller.rb
module Overrides
class SessionsController < DeviseTokenAuth::SessionsController
skip_before_action :authenticate_user_with_filter
before_action :set_country_by_ip, :only => [:create]
before_action :create_facebook_user, :only => [:create]
def create
# Check
field = (resource_params.keys.map(&:to_sym) & resource_class.authentication_keys).first
@resource = nil
if field
q_value = resource_params[field]
if resource_class.case_insensitive_keys.include?(field)
q_value.downcase!
end
#q = "#{field.to_s} = ? AND provider='email'"
q = "#{field.to_s} = ? AND provider='#{params[:provider]}'"
#if ActiveRecord::Base.connection.adapter_name.downcase.starts_with? 'mysql'
# q = "BINARY " + q
#end
@resource = resource_class.where(q, q_value).first
end
#sign in will be successful if @resource exists (matching user was found) and is a facebook login OR (email login and password matches)
if @resource and (params[:provider] == 'facebook' || (valid_params?(field, q_value) and @resource.valid_password?(resource_params[:password]) and (!@resource.respond_to?(:active_for_authentication?) or @resource.active_for_authentication?)))
# create client id
@client_id = SecureRandom.urlsafe_base64(nil, false)
@token = SecureRandom.urlsafe_base64(nil, false)
@resource.tokens[@client_id] = { token: BCrypt::Password.create(@token), expiry: (Time.now + DeviseTokenAuth.token_lifespan).to_i }
@resource.save
sign_in(:user, @resource, store: false, bypass: false)
yield @resource if block_given?
#render_create_success
render json: { data: resource_data(resource_json: @resource.token_validation_response) }
elsif @resource and not (!@resource.respond_to?(:active_for_authentication?) or @resource.active_for_authentication?)
render_create_error_not_confirmed
else
render_create_error_bad_credentials
end
end
def set_country_by_ip
if !params['fb_code'].blank?
if !params['user_ip'].blank?
#checks if IP sent is valid, otherwise raise an error
raise 'Invalid IP' unless (params['user_ip'] =~ Resolv::IPv4::Regex ? true : false)
country_code = Custom::FacesLibrary.get_country_by_ip(params['user_ip'])
country_id = Country.find_by(country_code: country_code)
if country_id
params.merge!(country_id: country_id.id, country_name: country_id.name, test: 'Test')
I18n.locale = country_id.language_code
else
params.merge!(country_id: 1, country_name: 'International')
end
else
params.merge!(country_id: 1, country_name: 'International')
end
end
end
def create_facebook_user
if !params['fb_code'].blank?
# TODO capture errors for invalid, expired or already used codes to return beter errors in API
user_info, access_token = Omniauth::Facebook.authenticate(params['fb_code'])
if user_info['email'].blank?
Omniauth::Facebook.deauthorize(access_token)
end
#if Facebook user does not exist create it
@user = User.find_by('uid = ? and provider = ?', user_info['id'], 'facebook')
if !@user
@graph = Koala::Facebook::API.new(access_token, ENV['FACEBOOK_APP_SECRET'])
Koala.config.api_version = "v2.6"
new_user_picture = @graph.get_picture_data(user_info['id'], type: :normal)
new_user_info = {
uid: user_info['id'],
provider: 'facebook',
email: user_info['email'],
name: user_info['name'],
first_name: user_info['first_name'],
last_name: user_info['last_name'],
image: new_user_picture['data']['url'],
gender: user_info['gender'],
fb_auth_token: access_token,
friend_count: user_info['friends']['summary']['total_count'],
friends: user_info['friends']['data']
}
@user = User.new(new_user_info)
@user.password = Devise.friendly_token.first(8)
@user.country_id = params['country_id']
@user.country_name = params['country_name']
if !@user.save
render json: @user.errors, status: :unprocessable_entity
end
end
#regardless of user creation, merge facebook parameters for proper sign_in in standard action
params.merge!(provider: 'facebook', email: @user.email)
else
params.merge!(provider: 'email')
end
end
end
end
Notice the use of params.merge!
in the callback to add custom parameters to the main controller methods. This is a nifty trick that unfortunately will be be deprecated in Rails 5.1 as params will no longer inherit from hash.
Method #2
If you just want to add functionality to a method in your custom controller, you can get away with subclassing a controller, inheriting from the original controller and passing a block to super as described here:
https://github.com/lynndylanhurley/devise_token_auth#passing-blocks-to-controllers
I have done this to the create method in my custom registrations controller.
Modify the routes as in method #1
#config/routes.rb
...
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
sessions: 'overrides/sessions',
registrations: 'overrides/registrations'
}
...
and customize the create
method in the custom controller:
#app/controllers/overrides/registrations_controller.rb
module Overrides
class RegistrationsController < DeviseTokenAuth::RegistrationsController
skip_before_action :authenticate_user_with_filter
#will run upon creating a new registration and will set the country_id and locale parameters
#based on whether or not a user_ip param is sent with the request
#will default to country_id=1 and locale='en' (International) if it's not sent.
before_action :set_country_and_locale_by_ip, :only => [:create]
def set_country_and_locale_by_ip
if !params['user_ip'].blank?
#checks if IP sent is valid, otherwise raise an error
raise 'Invalid IP' unless (params['user_ip'] =~ Resolv::IPv4::Regex ? true : false)
country_code = Custom::FacesLibrary.get_country_by_ip(params['user_ip'])
#TODO check if there's an internet connection here or inside the library function
#params.merge!(country_id: 1, country_name: 'International', locale: 'en')
country_id = Country.find_by(country_code: country_code)
if country_id
params.merge!(country_id: country_id.id, locale: country_id.language_code, country_name: country_id.name)
else
params.merge!(country_id: 1, country_name: 'International', locale: 'en')
end
else
params.merge!(country_id: 1, country_name: 'International', locale: 'en')
end
end
#this will add behaviour to the registrations controller create method
def create
super do |resource|
create_assets(@resource)
end
end
def create_assets(user)
begin
Asset.create(user_id: user.id, name: "stars", qty: 50)
Asset.create(user_id: user.id, name: "lives", qty: 5)
Asset.create(user_id: user.id, name: "trophies", qty: 0)
end
end
end
end