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.
For the specific serialiser question, here's how I did it:
overrides/sessions_controller.rb
module Api
module V1
module Overrides
class SessionsController < ::DeviseTokenAuth::SessionsController
# override this method to customise how the resource is rendered. in this case an ActiveModelSerializers 0.10 serializer.
def render_create_success
render json: { data: ActiveModelSerializers::SerializableResource.new(#resource).as_json }
end
end
end
end
end
config/routes.rb
namespace :api, defaults: {format: 'json'} do
scope module: :v1, constraints: ApiConstraints.new(version: 1, default: true) do
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
sessions: 'api/v1/overrides/sessions'
}
# snip the rest
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
Related
The following action
def send(server=1)
#messagelog = Messagelog.new(server_id: params[:server], struttura_id: params[:struttura], user_id: params[:user], chat_id: params[:chat], methodtype_id: params[:methodtype], payload: params[:payload])
#messagelog.save
bot = Telegram.bot
case params[:methodtype]
when 1
result = bot.send_message(chat_id: params[:chat], text: params[:payload])
when 2
result = bot.send_photo(chat_id: params[:chat], photo: params[:payload])
when 3
result = bot.send_document(chat_id: params[:chat], document: params[:payload])
else
end
#messagelog.update_attributes(result: result.to_json)
rescue StandardError => msg
end
is invoked via an API and runs 5 times, rescued or not. Its route is:
namespace :api do
namespace :v1 do
post 'send', to: 'messages#send', defaults: { format: :json }
The class class Api::V1::MessagesController < Api::V1::ApibaseController does not invoke before_actions, however it inherits
class Api::V1::ApibaseController < ApplicationController
before_action :access_control
def access_control
authenticate_or_request_with_http_token do |token, options|
#server = Server.where('apikey = ?', token).first
end
end
Where is this multiplication of actions originating from? The only hiccup from the logs is a statment that:
No template found for Api::V1::MessagesController#send, rendering head :no_content
If the param are hard-wired to the action, this is also generated, but only one action occurs. Rails 5.2.4
How can this be resolved?
I have a rails application in which I'm trying to add a future to ban existing users. My react request is as follows:
handleUserBan(evt) {
evt.preventDefault();
let user_id = evt.target.dataset.userId;
API.put('admin/users/'+user_id, {user: {banned: true}}, function(res) {
this.loadUsers();
}.bind(this))
}
And my 'UsersController' inside admin namespace is:
before_action :enforce_admin!
def show
#user = User.find(ban_params[:id])
end
def update
#user = User.find(ban_params[:id])
prms = ban_params
if prms.include?(:banned)
#user.update_attributes!(prms)
#user.save!
return render :status=>200, :json => {success: true}
end
end
private
def ban_params
params.require(:user).permit(:banned)
end
But I'm getting an error:
ActiveRecord: Record not found
Couldn't find User with 'id'=
Even though a user exists with the selected id in my database. My request is structured as follows:
Request
Parameters:
{"user"=>{"banned"=>"true"},
"id"=>"7",
"format"=>"json"}
And here are my routes for admin namespace:
namespace :admin do
put 'ban_user', :to => 'users#ban_user'
resources :charges
resources :coaches
resources :events
resources :invoices
resources :reviews
resources :users
end
try just params[:id] instead of ban_params[:id]
method ban_params will return only value of banned from params. In this case params contains { id: 'user_id', action: "your action", controller: 'controller', ..., user: { banned: true } }
def ban_params
params.require(:user).permit(:banned)
end
This code is filtering out the id parameter, since it's only permitting the banned parameter:
def ban_params
params.require(:user).permit(:banned)
end
Something like this might work, although it loses the permit constraint:
params.permit(:id, :user => [:banned])
Can you give an advice or recommend some resources related to this topic? I understand how to it in a theory. But I also heard about jwt etc. What are the best practices to implement device/angular/rails role based auth/registration?
The short answer is to read this blog post which goes into details of how the concept is minimally implemented
This would be a long code answer, but I plan to write separate blog post on how to implement it in much more details...
but for now, here is how I implemented it in some project...
First the angular app part, you can use something like Satellizer which plays nicely...
here is the angular auth module in the front-end app
# coffeescript
config = (
$authProvider
$stateProvider
) ->
$authProvider.httpInterceptor = true # to automatically add the headers for auth
$authProvider.baseUrl = "http://path.to.your.api/"
$authProvider.loginRedirect = '/profile' # front-end route after login
$authProvider.logoutRedirect = '/' # front-end route after logout
$authProvider.signupRedirect = '/sign_in'
$authProvider.loginUrl = '/auth/sign_in' # api route for sign_in
$authProvider.signupUrl = '/auth/sign_up' # api route for sign_up
$authProvider.loginRoute = 'sign_in' # front-end route for login
$authProvider.signupRoute = 'sign_up' # front-end route for sign_up
$authProvider.signoutRoute = 'sign_out' # front-end route for sign_out
$authProvider.tokenRoot = 'data'
$authProvider.tokenName = 'token'
$authProvider.tokenPrefix = 'front-end-prefix-in-localstorage'
$authProvider.authHeader = 'Authorization'
$authProvider.authToken = 'Bearer'
$authProvider.storage = 'localStorage'
# state configurations for the routes
$stateProvider
.state 'auth',
url: '/'
abstract: true
templateUrl: 'modules/auth/auth.html'
data:
permissions:
only: ['guest']
redirectTo: 'profile'
.state 'auth.sign_up',
url: $authProvider.signupRoute
views:
'sign_up#auth':
templateUrl: 'modules/auth/sign_up.html'
controller: 'AuthenticationCtrl'
controllerAs: 'vm'
.state 'auth.sign_in',
url: $authProvider.loginRoute
views:
'sign_in#auth':
templateUrl: 'modules/auth/sign_in.html'
controller: 'AuthenticationCtrl'
controllerAs: 'vm'
this is the basic configurations for satellizer... as for the authentication controller... it's something like following
#signIn = (email, password, remember_me) ->
$auth.login
email: email
password: password
remember_me: remember_me
.then(success, error)
return
#signUp = (name, email, password) ->
$auth.signup
name: name
email: email
password: password
.then(success, error)
return
this is the basics for authenticating
as for the backend (RoR API) you should first allow CORS for the front-end app. and add gem 'jwt' to your gemfile.
second implement the API controller and the authentication controller
for example it might look something like the following
class Api::V1::ApiController < ApplicationController
# The API responds only to JSON
respond_to :json
before_action :authenticate_user!
protected
def authenticate_user!
http_authorization_header?
authenticate_request
set_current_user
end
# Bad Request if http authorization header missing
def http_authorization_header?
fail BadRequestError, 'errors.auth.missing_header' unless authorization_header
true
end
def authenticate_request
decoded_token ||= AuthenticationToken.decode(authorization_header)
#auth_token ||= AuthenticationToken.where(id: decoded_token['id']).
first unless decoded_token.nil?
fail UnauthorizedError, 'errors.auth.invalid_token' if #auth_token.nil?
end
def set_current_user
#current_user ||= #auth_token.user
end
# JWT's are stored in the Authorization header using this format:
# Bearer some_random_string.encoded_payload.another_random_string
def authorization_header
return #authorization_header if defined? #authorization_header
#authorization_header =
begin
if request.headers['Authorization'].present?
request.headers['Authorization'].split(' ').last
else
nil
end
end
end
end
class Api::V1::AuthenticationsController < Api::V1::ApiController
skip_before_action :authenticate_user!, only: [:sign_up, :sign_in]
def sign_in
# getting the current user from sign in request
#current_user ||= User.find_by_credentials(auth_params)
fail UnauthorizedError, 'errors.auth.invalid_credentials' unless #current_user
generate_auth_token(auth_params)
render :authentication, status: 201
end
def sign_out
# this auth token is assigned via api controller from headers
#auth_token.destroy!
head status: 204
end
def generate_auth_token(params)
#auth_token = AuthenticationToken.generate(#current_user, params[:remember_me])
end
end
The AuthenticationToken is a model used to keep track of the JWT tokens ( for session management like facebook)
here is the implementation for the AuthenticationToken model
class AuthenticationToken < ActiveRecord::Base
## Relations
belongs_to :user
## JWT wrappers
def self.encode(payload)
AuthToken.encode(payload)
end
def self.decode(token)
AuthToken.decode(token)
end
# generate and save new authentication token for the user
def self.generate(user, remember_me = false)
#auth_token = user.authentication_tokens.create
#auth_token.token = AuthToken.generate(#auth_token.id, remember_me)
#auth_token.save!
#auth_token
end
# check if a token can be used or not
# used by background job to clear the authentication collection
def expired?
AuthToken.decode(token).nil?
end
end
it uses a wrapper called AuthToken which wraps the JWT functionality
here is it's implementation
# wrapper around JWT to encapsulate it's code
# and exception handling and don't polute the AuthenticationToken model
class AuthToken
def self.encode(payload)
JWT.encode(payload, Rails.application.secrets.secret_key_base)
end
def self.decode(token)
payload = JWT.decode(token, Rails.application.secrets.secret_key_base)[0]
rescue JWT::ExpiredSignature
# It will raise an error if it is not a token that was generated
# with our secret key or if the user changes the contents of the payload
Rails.logger.info "Expired Token"
nil
rescue
Rails.logger.warn "Invalid Token"
nil
end
def self.generate(token_id, remember_me = false)
exp = remember_me ? 6.months.from_now : 6.hours.from_now
payload = { id: token_id.to_s, exp: exp.to_i }
self.encode(payload)
end
end
I am trying to authenticate my new Shopify app. First, my authenticate method redirects the shop owner to Shopify's authentication page:
def authenticate
ShopifyAPI::Session.setup({:api_key => "123", :secret => "456"})
session = ShopifyAPI::Session.new("someshop.myshopify.com")
redirect_to session.create_permission_url(["read_orders"], "https://myapp.com/shopify/post_authenticate?user=someshop")
end
Once the shop owner has approved the integration, the redirect uri triggers my post_authenticate method:
def post_authenticate
ShopifyAPI::Session.setup({:api_key => "123", :secret => "456"})
session = ShopifyAPI::Session.new("#{params[:user]}.myshopify.com")
token = session.request_token(:code => params[:code], :signature => params[:signature], :timestamp => params[:timestamp])
end
But the request_token method returns the following error:
#<ShopifyAPI::ValidationException: Invalid Signature: Possible malicious login>
I have read somewhere that you need to be in the same ShopifyAPI session while doing all of this, but it does not say so in the documentation. And the example app takes a very different approach than the documentation.
As per my comment, I utilize the omniauth methodology for authenticating. Here's a gist of the code for reference. https://gist.github.com/agmcleod/7106363317ebd082d3df. Put all the snippets below.
class ApplicationController < ActionController::Base
protect_from_forgery
force_ssl
helper_method :current_shop, :shopify_session
protected
def current_shop
#current_shop ||= Shop.find(session[:shop_id]) if session[:shop_id].present?
end
def shopify_session
if current_shop.nil?
redirect_to new_login_url
else
begin
session = ShopifyAPI::Session.new(current_shop.url, current_shop.token)
ShopifyAPI::Base.activate_session(session)
yield
ensure
ShopifyAPI::Base.clear_session
end
end
end
end
In my login controller:
def create
omniauth = request.env['omniauth.auth']
if omniauth && omniauth[:provider] && omniauth[:provider] == "shopify"
shop = Shop.find_or_create_by_url(params[:shop].gsub(/https?\:\/\//, ""))
shop.update_attribute(:token, omniauth['credentials'].token)
shopify_session = ShopifyAPI::Session.new(shop.url, shop.token)
session[:shop_id] = shop.to_param
redirect_to root_url
else
flash[:error] = "Something went wrong"
redirect_to root_url
end
end
config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
provider :shopify, Settings.api_key, Settings.api_secret,
scope: 'write_products,write_script_tags,read_orders',
setup: lambda { |env| params = Rack::Utils.parse_query(env['QUERY_STRING'])
env['omniauth.strategy'].options[:client_options][:site] = "http://#{params['shop']}" }
end
Then in your routes file, map the create action of your session appropriately:
match '/auth/shopify/callback', :to => 'login#create'
From there i use the shopify_session method as an around filter on the appropriate controllers.
I'm using devise_invitable with devise in my Rails 3 app. I want to give my users the ability to invite other users and group those invited users in advance of the invitees signing up.
My problem is that once the invitee comes along and signs up (but doesn't use the invite URL), the destroy_if_previously_invited around_filter comes along, destroys the original user record and recreates a new record for the user, retaining the invitation data but not transferring the user_groups records along with it.
I'd like to simply override this around_filter by doing a search for any user_groups that match the originally invited user_id and saving them with the newly created user_id.
I keep getting the error:
LocalJumpError in Users::RegistrationsController#create
no block given (yield)
My route looks like this:
devise_for :users, :controllers => { :registrations => "users/registrations" }
I've set this as the override in app/controllers/users/registrations_controller.rb:
class Users::RegistrationsController < Devise::RegistrationsController
around_filter :destroy_if_previously_invited, :only => :create
private
def destroy_if_previously_invited
invitation_info = {}
user_hash = params[:user]
if user_hash && user_hash[:email]
#user = User.find_by_email_and_encrypted_password(user_hash[:email], '')
if #user
invitation_info[:invitation_sent_at] = #user[:invitation_sent_at]
invitation_info[:invited_by_id] = #user[:invited_by_id]
invitation_info[:invited_by_type] = #user[:invited_by_type]
invitation_info[:user_id] = #user[:id]
#user.destroy
end
end
# execute the action (create)
yield
# Note that the after_filter is executed at THIS position !
# Restore info about the last invitation (for later reference)
# Reset the invitation_info only, if invited_by_id is still nil at this stage:
#user = User.find_by_email_and_invited_by_id(user_hash[:email], nil)
if #user
#user[:invitation_sent_at] = invitation_info[:invitation_sent_at]
#user[:invited_by_id] = invitation_info[:invited_by_id]
#user[:invited_by_type] = invitation_info[:invited_by_type]
user_groups = UserGroup.find_all_by_user_id(invitation_info[:user_id])
for user_group in user_groups do
user_group.user_id = #user.id
user_group.save!
end
#user.save!
end
end
end
I may also just be going about this all wrong. Any ideas would be appreciated.
First, there is no need to set the around_filter again, as devise_invitable already sets this. All you need to do is redefine the methods in your UsersController and those will be called instead of the devise_invitable ones.
Second, it looks like you are combining the two methods when they should remain seperate and overridden (I am basing this off latest version of devise_invitatable 1.1.1)
Try something like this:
class Users::RegistrationsController < Devise::RegistrationsController
protected
def destroy_if_previously_invited
hash = params[resource_name]
if hash && hash[:email]
resource = resource_class.where(:email => hash[:email], :encrypted_password => '').first
if resource
#old_id = resource.id #saving the old id for use in reset_invitation_info
#invitation_info = Hash[resource.invitation_fields.map {|field|
[field, resource.send(field)]
}]
resource.destroy
end
end
end
def reset_invitation_info
# Restore info about the last invitation (for later reference)
# Reset the invitation_info only, if invited_by_id is still nil at this stage:
resource = resource_class.where(:email => params[resource_name][:email], :invited_by_id => nil).first
if resource && #invitation_info
resource.invitation_fields.each do |field|
resource.send("#{field}=", #invitation_info[field])
end
### Adding your code here
if #old_id
user_groups = UserGroup.find_all_by_user_id(#old_id)
for user_group in user_groups do
user_group.user_id = resource.id
user_group.save!
end
end
### End of your code
resource.save!
end
end