使用devise_token_auth和active_model_serializer

时间:2016-05-29 22:58:45

标签: ruby-on-rails devise active-model-serializers

在使用devise_token_auth和active_model_serializer for Devise sign_up方法时,我无法覆盖Rails序列化程序。

我想在查询API时自定义Devise sign_up控制器返回的字段。

devise_token_auth gem文档指出:

要自定义json渲染,请实现以下受保护的控制器方法

注册控制器

...

render_create_success

...

注意:控制器覆盖必须实现它们替换的控制器的预期操作。

这一切都很好,但我该怎么做?

我已尝试生成如下的UserController序列化程序:

class UsersController < ApplicationController

  def default_serializer_options
    { serializer: UserSerializer }
  end

  # GET /users
  def index
    @users = User.all

    render json: @users
  end

end

但它只用于自定义方法,例如上面的索引方法:它没有像sign_up这样的设计方法获取

我很感激,因为我一直到处寻找详细的回复,但我一次只得到一块拼图。

2 个答案:

答案 0 :(得分:2)

Devise sign_up对应devise_token_auth注册控制器和Devise sign_in对应devise_token_auth会话控制器。因此,在使用此gem时,自定义Devise sign_in和sign_up方法需要自定义这两个devise_token_auth控制器。

根据您需要完成的任务,有两种方法可以实现。

方法#1

如果要完全自定义控制器中的方法,请按照文档覆盖devise_token_auth控制器方法:https://github.com/lynndylanhurley/devise_token_auth#custom-controller-overrides

这就是我所做的,它的工作正常:

#config/routes.rb

...

mount_devise_token_auth_for 'User', at: 'auth', controllers: {
    sessions: 'overrides/sessions',
    registrations: 'overrides/registrations'
  }

...

如果本地控制器覆盖中存在方法,则会将所有devise_token_auth会话和注册路由到LOCAL版本的控制器。如果您的本地覆盖中不存在该方法,那么它将从gem运行该方法。您基本上必须将控制器从gem复制到&#39; app / controllers / overrides&#39;并对您需要自定义的任何方法进行任何更改。从您未自定义的本地副本中删除方法。您也可以通过这种方式添加回调。如果要修改响应,请在方法末尾自定义渲染,该渲染将通过active_model_serializer将响应返回为json。

这是我的会话控制器的一个示例,它添加了几个自定义before_actions来添加自定义功能:

#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

请注意在回调中使用params.merge!将自定义参数添加到主控制器方法。这是一个非常好的技巧,遗憾的是在Rails 5.1中将不推荐使用,因为params将不再继承hash。

方法#2

如果您只想为自定义控制器中的方法添加功能,则可以使用子类化控制器,继承原始控制器并将块传递给super,如下所述:

https://github.com/lynndylanhurley/devise_token_auth#passing-blocks-to-controllers

我已经在自定义注册控制器中对create方法执行了此操作。

按方法#1

修改路线
#config/routes.rb

...

mount_devise_token_auth_for 'User', at: 'auth', controllers: {
    sessions: 'overrides/sessions',
    registrations: 'overrides/registrations'
  }

...

并自定义自定义控制器中的create方法:

#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

答案 1 :(得分:2)

对于特定的序列化问题,我的方法如下:

覆盖/ 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

配置/ 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