devise_token_authのregistrations_controllerのcreateアクションのソースコードを追ってみる

こんにちは!kossyです!



さて、今回はOSSソースコードリーディング回ということで、devise_token_authのregistrations_controllerのcreateアクションのソースコードを読んでみたので、ブログに残してみたいと思います。



コードリーディング

registrations_controllerのcreateアクションは、ユーザー登録を担うアクションになっています。

devise_token_auth/registrations_controller.rb at master · lynndylanhurley/devise_token_auth · GitHub

    def create
      build_resource

      unless @resource.present?
        raise DeviseTokenAuth::Errors::NoResourceDefinedError,
              "#{self.class.name} #build_resource does not define @resource,"\
              ' execution stopped.'
      end

      # give redirect value from params priority
      @redirect_url = params.fetch(
        :confirm_success_url,
        DeviseTokenAuth.default_confirm_success_url
      )

      # success redirect url is required
      if confirmable_enabled? && !@redirect_url
        return render_create_error_missing_confirm_success_url
      end

      # if whitelist is set, validate redirect_url against whitelist
      return render_create_error_redirect_url_not_allowed if blacklisted_redirect_url?(@redirect_url)

      # override email confirmation, must be sent manually from ctrl
      callback_name = defined?(ActiveRecord) && resource_class < ActiveRecord::Base ? :commit : :create
      resource_class.set_callback(callback_name, :after, :send_on_create_confirmation_instructions)
      resource_class.skip_callback(callback_name, :after, :send_on_create_confirmation_instructions)

      if @resource.respond_to? :skip_confirmation_notification!
        # Fix duplicate e-mails by disabling Devise confirmation e-mail
        @resource.skip_confirmation_notification!
      end

      if @resource.save
        yield @resource if block_given?

        unless @resource.confirmed?
          # user will require email authentication
          @resource.send_confirmation_instructions({
            client_config: params[:config_name],
            redirect_url: @redirect_url
          })
        end

        if active_for_authentication?
          # email auth has been bypassed, authenticate user
          @token = @resource.create_token
          @resource.save!
          update_auth_header
        end

        render_create_success
      else
        clean_up_passwords @resource
        render_create_error
      end
    end

before_actionが呼ばれるのでまずはそちらから読んでみます。

# https://github.com/lynndylanhurley/devise_token_auth/blob/4c5245b88b39c1bb305e0cbdbfc2513eebdeda93/app/controllers/devise_token_auth/registrations_controller.rb#L189


def validate_sign_up_params
  validate_post_data sign_up_params, I18n.t('errors.messages.validate_sign_up_params')
end

# https://github.com/lynndylanhurley/devise_token_auth/blob/4c5245b88b39c1bb305e0cbdbfc2513eebdeda93/app/controllers/devise_token_auth/registrations_controller.rb#L189

def validate_post_data which, message
  render_error(:unprocessable_entity, message, status: 'error') if which.empty?
end

# https://github.com/lynndylanhurley/devise_token_auth/blob/4c5245b88b39c1bb305e0cbdbfc2513eebdeda93/app/controllers/devise_token_auth/registrations_controller.rb#L91

def sign_up_params
  params.permit(*params_for_resource(:sign_up))
end

sign_up_paramsが空の時かどうかをチェックして、空の場合はエラーを返すバリデーションでした。

こういった、実際にcontroller内で値の処理を行う前にパラメータに対して何かしらのバリデーションを実行するのは定石ですね。

次にbuild_resourceメソッドを読んでみます。

devise_token_auth/registrations_controller.rb at master · lynndylanhurley/devise_token_auth · GitHub

def build_resource
  @resource            = resource_class.new(sign_up_params)
  @resource.provider   = provider

  # honor devise configuration for case_insensitive_keys
  if resource_class.case_insensitive_keys.include?(:email)
    @resource.email = sign_up_params[:email].try(:downcase)
  else
    @resource.email = sign_up_params[:email]
  end
end

resource_classはdeviseをincludeしているモデルのことですね。例えばUserモデルが定義されていた場合は、

@resource = User.new(sign_up_params)

となります。次の処理の「provider」はdefaultでemailになっています。

resource_class.case_insensitive_keys.include?(:email)はdeviseの定義が存在していればそちらを優先して処理を行います。

deviseの定義はconfig/initializers/devise.rbの、

  # Configure which authentication keys should be case-insensitive.
  # These keys will be downcased upon creating or modifying a user and when used
  # to authenticate or find a user. Default is :email.
  config.case_insensitive_keys = [:email]

の部分です。

case_insensitive_keysにemailが含まれている場合は、emailをdowncaseして、そうでない場合はパラメータで送信されたemailをそのままresourceのattrとして代入しています。

createアクションに戻ります。

unless @resource.present?
  raise DeviseTokenAuth::Errors::NoResourceDefinedError,
        "#{self.class.name} #build_resource does not define @resource,"\
        ' execution stopped.'
end

もしbuild_resourceを実行して、@resourceが存在しない場合は、例外を発生させています。

次の処理は、リダイレクト先のURLを定義しています。

その次のif文は、deviseをincludeしているモデルがconfirmableモジュールをincludeしていて、redirect_urlがnilの場合、「リダイレクト先のURLがない」というエラーを返却していました。

blacklisted_redirect_url?は内部の処理を見ましょう。

devise_token_auth/application_controller.rb at 4c5245b88b39c1bb305e0cbdbfc2513eebdeda93 · lynndylanhurley/devise_token_auth · GitHub

def blacklisted_redirect_url?(redirect_url)
  DeviseTokenAuth.redirect_whitelist && !DeviseTokenAuth::Url.whitelisted?(redirect_url)
end

devise_token_authのconfigファイルでホワイトリストを設定していて、かつ引数のredirect_urlがホワイトリストで設定されたURLでない場合はtrueを返すメソッドでした。

次の行を見てみます。

callback_name = defined?(ActiveRecord) && resource_class < ActiveRecord::Base ? :commit : :create
resource_class.set_callback(callback_name, :after, :send_on_create_confirmation_instructions)
resource_class.skip_callback(callback_name, :after, :send_on_create_confirmation_instructions)

ActiveRecordが定義されていて、かつresource_classがActiveRecordを継承していた場合、:commitが、そうでなければ :create がcallback_nameとして定義されていました。

その次の行はresource_classに対してcallback_nameのcallbackとskip_callbackをセットしていました。

次の行は、実質的にresource_classがconfirmableモジュールをincludeしているかどうかを判定しています。

devise/confirmable.rb at master · heartcombo/devise · GitHub

疲れてきたので最後はソースコードに適宜コメントを入れて書いていきます、、、

      # saveに成功した場合
      if @resource.save

        # ブロックが渡されていた場合は渡されたブロック内で@resourceを使って処理
        yield @resource if block_given?

        # 確認済みでない場合(confired_atに値がない場合)
        unless @resource.confirmed?
          # user will require email authentication
          @resource.send_confirmation_instructions({
            client_config: params[:config_name],
            redirect_url: @redirect_url
          })
        end

        # resource_classがdatabase_authenticatableをincludeしている場合
        if active_for_authentication?
          # email auth has been bypassed, authenticate user
          @token = @resource.create_token
          @resource.save!
          update_auth_header
        end

        render_create_success
      else
        clean_up_passwords @resource
        render_create_error
      end

これでひと通り目を通せました。

おわりに

confirmableモジュールを使っている場合の条件分岐がそこそこあったので、ユーザー登録後の確認メール機能を実装する場合はこのブログが役に立ってくれること願っています。