LoginSignup
10
14

More than 3 years have passed since last update.

Rails初心者がdeviseのsessions#createを読み解く(途中)

Last updated at Posted at 2018-12-27

ログインをmodalで実装してるんだけど、validationにひっかかったときに
registrations#newに遷移してしまう。
→modal上で完結させたい。

devise触りたてでソース読んでないので、流れとか整理しておく。

devise/registrations_controller.rb
  def create
    build_resource(sign_up_params) #01
    resource.save
    yield resource if block_given?
    if resource.persisted?
      if resource.active_for_authentication? #02
        set_flash_message! :notice, :signed_up #03
        sign_up(resource_name, resource) #04
        respond_with resource, location: after_sign_up_path_for(resource) #05
      else
        set_flash_message! :notice, : "signed_up_but_#{resource.inactive_message}"
        expire_data_after_sign_in! #[06]
        respond_with resource, location: after_inactive_sign_up_path_for(resource) #07
      end
    else
      clean_up_passwords resource #08
      set_minimum_password_length #09
      respond_with resource
    end
  end

01 build_resource(sign_up_params)

devise/registrations_controller.rb
  # Build a devise resource passing in the session. Useful to move
  # temporary session data to the newly created user.
  def build_resource(hash = {})
    self.resource = resource_class.new_with_session(hash, session)
  end

●recource_classに関して

devise/devise_controller.rb
 # Proxy to devise map class
  def resource_class
    devise_mapping.to
  end
devise/devise_controller.rb
  # Attempt to find the mapped route for devise based on request path
  def devise_mapping
    @devise_mapping ||= request.env["devise.mapping"]
  end
lib/devise/mapping.rb
  # Gives the class the mapping points to.
  def to
    @klass.get
  end

resource_class = Devise.mappingsの@klassってことね。

request.env:
https://qiita.com/kentosasa/items/a0500f2f21b33b29beb6

●new_with_sessionに関して

devise/test/rails_app/lib/shared_user.rb
  module ExtendMethods
    def new_with_session(params, session)
      super.tap do |user|
        if data = session["devise.facebook_data"]
          user.email = data["email"]
          user.confirmed_at = Time.now
        end
      end
    end
  end

.tap(Object)
obj.tap {|myself| block }
tapメソッドは、ブロック変数にレシーバ自身を入れてブロックを実行します。
戻り値はレシーバ自身です。メソッドチェーンの中にtapメソッドをはさみ込み、
ソースコードを簡潔にする目的で使われます。
https://ref.xaio.jp/ruby/classes/object/tap

Facebook認証の場合、
resource_class(つまりは@klass)に対して.tapをするのかな?
Facebook認証じゃない場合は、そのまんま。

というわけで、
build_resource(sign_up_params)
→受け取ったパラメータの中にsession["devise.facebook_data"]があれば、
→user.emailをそのパラメータの中に入っている:emailで上書きする
→confirmed_atは現在の(上書きした)時間

facebook認証用のもの(?)

02 active_for_authentication?

●active_for_authentication?に関して

devise/models/authenticatable.rb
def active_for_authentication?
  true
end

えっ。
と思ったらこんな注意書きが

devise/models/authenticatable.rb
   # == active_for_authentication?
    #
    # After authenticating a user and in each request, Devise checks if your model is active by
    # calling model.active_for_authentication?. This method is overwritten by other devise modules. For instance,
    # :confirmable overwrites .active_for_authentication? to only return true if your model was confirmed.
    #
    # You can overwrite this method yourself, but if you do, don't forget to call super:
    #
    #   def active_for_authentication?
    #     super && special_condition_is_valid?
    #   end
    #
    # Whenever active_for_authentication? returns false, Devise asks the reason why your model is inactive using
    # the inactive_message method. You can overwrite it as well:
    #
    #   def inactive_message
    #     special_condition_is_valid? ? super : :special_condition_is_not_valid
    #   end
    #
ユーザを認証した後、deviseは`model.active_for_authentication?`を実行してモデルが本当にアクティブかを調べる。
このメソッドは他のdevice modulesによってオーバライトされる。
例えば、:confirmableモデルでは、モデルが確認できたらtrueを返すようにオーバライトしている。

オーバライトしてもいいけど、superを呼ぶのを忘れるなよ。


このメソッドがfalseを返すと、deviseはなんでモデルがアクティブじゃねーんだよ、と
inactive_messageメソッドを通じて聞いてくる。これもオーバライトしてよい

オーバライト前提だからtrueしか返してないんですかね。。とりあえず次

03 set_flash_message! :notice, :signed_up

devise/devise_controller.rb
  def set_flash_message(key, kind, options = {})
    message = find_message(kind, options) #refer below
    if options[:now]
      flash.now[key] = message if message.present?
    else
      flash[key] = message if message.present?
    end
  end

keyが:notice、:kindが:signed_upに対応してますね。
messageってのがあれば、messageをflashの中に入れるんですね。
それがoptions[:now]があるかどうかで入れ方が変わると。
んでそのmessageってのは、find_message(:signed_up)だと。

devise/devise_controller.rb
  # Get message for given
  def find_message(kind, options = {})
    options[:scope] ||= translation_scope #refer below
    options[:default] = Array(options[:default]).unshift(kind.to_sym)
    options[:resource_name] = resource_name
    options = devise_i18n_options(options) #refer below
    I18n.t("#{options[:resource_name]}.#{kind}", options)
  end

I18n.t→I18n.translateの略。
翻訳したのをfind_messageの戻り値として返してくれるんですかね。
https://railsguides.jp/i18n.html
https://qiita.com/Kta-M/items/bd4ba36a58ad602a9d8b

devise/devise_controller.rb
  # Controllers inheriting DeviseController are advised to override this
  # method so that other controllers inheriting from them would use
  # existing translations.
  def translation_scope
    "devise.#{controller_name}"
  end
  def devise_i18n_options(options)
    options
  end

というわけで大まかにいうと、
set_flash_message! :notice, :signed_up

flash[:notice] = :signed_up

flashを規定するもの(まあメソッド名からもわかるが

04 sign_up(resource_name, resource)

/app/controllers/devise/registrations_controller.rb
  # Signs in a user on sign up. You can overwrite this method in your own
  # RegistrationsController.
  def sign_up(resource_name, resource)
    sign_in(resource_name, resource)
  end
lib/devise/controllers/sign_in_out.rb
      # Sign in a user that already was authenticated. This helper is useful for logging
      # users in after sign up. All options given to sign_in is passed forward
      # to the set_user method in warden.
      # If you are using a custom warden strategy and the timeoutable module, you have to
      # set `env["devise.skip_timeout"] = true` in the request to use this method, like we do
      # in the sessions controller: https://github.com/plataformatec/devise/blob/master/app/controllers/devise/sessions_controller.rb#L7
      #
      # Examples:
      #
      #   sign_in :user, @user                      # sign_in(scope, resource)
      #   sign_in @user                             # sign_in(resource)
      #   sign_in @user, event: :authentication     # sign_in(resource, options)
      #   sign_in @user, store: false               # sign_in(resource, options)
      #
      def sign_in(resource_or_scope, *args)
        options  = args.extract_options!
        scope    = Devise::Mapping.find_scope!(resource_or_scope)
        resource = args.last || resource_or_scope

        expire_data_after_sign_in! #refer below

        if options[:bypass]
          ActiveSupport::Deprecation.warn(<<-DEPRECATION.strip_heredoc, caller)
          [Devise] bypass option is deprecated and it will be removed in future version of Devise.
          Please use bypass_sign_in method instead.
          Example:
            bypass_sign_in(user)
          DEPRECATION
          warden.session_serializer.store(resource, scope) #refer below
        elsif warden.user(scope) == resource && !options.delete(:force)
          # Do nothing. User already signed in and we are not forcing it.
          true
        else
          warden.set_user(resource, options.merge!(scope: scope))
        end
      end

Array#extract_options! で Rails API のように柔軟な引数を取るメソッドを定義する
http://www.techscore.com/blog/2012/12/25/arrayextract_options-%E3%81%A7-rails-api-%E3%81%AE%E3%82%88%E3%81%86%E3%81%AB%E6%9F%94%E8%BB%9F%E3%81%AA%E5%BC%95%E6%95%B0%E3%82%92%E5%8F%96%E3%82%8B%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%82%92/

lib/devise/controllers/sign_in_out.rb
     def expire_data_after_sign_in!
        # session.keys will return an empty array if the session is not yet loaded.
        # This is a bug in both Rack and Rails.
        # A call to #empty? forces the session to be loaded.
        session.empty?
        session.keys.grep(/^devise\./).each { |k| session.delete(k) }
      end

      alias :expire_data_after_sign_out! :expire_data_after_sign_in!

sessionが読み込まれないとsession.keysは空の配列を返してくる。
これはRackとRailsのバグだから、一回読み込んでやる必要がある。
そのためのsession.empty?だと。。

ただ単にsession消してるだけかな。

●warden.session_serializer.store(resource, scope)

lib/warden/proxy.rb
    # Points to a SessionSerializer instance responsible for handling
    # everything related with storing, fetching and removing the user
    # session.
    # :api: public
    def session_serializer
      @session_serializer ||= Warden::SessionSerializer.new(@env)
    end
lib/warden/session_serializer.rb
# encoding: utf-8
# frozen_string_literal: true
module Warden
  class SessionSerializer
    attr_reader :env

    def initialize(env)
      @env = env
    end

    def key_for(scope)
      "warden.user.#{scope}.key"
    end

    def serialize(user)
      user
    end

    def deserialize(key)
      key
    end

    def store(user, scope)
      return unless user
      method_name = "#{scope}_serialize"
      specialized = respond_to?(method_name)
      session[key_for(scope)] = specialized ? send(method_name, user) : serialize(user)
    end

    # 中略

  end # SessionSerializer
end # Warden

親玉みたいのが出てきた笑
storeもここに載ってる。
なんかsession機能の根幹になってそう。。

storeは、大まかに言うと
session[:scope] = user
を担ってるのね。実際にscope、userを入れているわけではないけど。

●warden.userについて

lib/warden/proxy.rb
    def user(argument = {})
      opts  = argument.is_a?(Hash) ? argument : { :scope => argument }
      scope = (opts[:scope] ||= @config.default_scope)

      if @users.has_key?(scope)
        @users[scope]
      else
        unless user = session_serializer.fetch(scope)
          run_callbacks = opts.fetch(:run_callbacks, true)
          manager._run_callbacks(:after_failed_fetch, user, self, :scope => scope) if run_callbacks
        end

        @users[scope] = user ? set_user(user, opts.merge(:event => :fetch)) : nil
      end
    end

● warden.set_userについて

lib/warden/proxy.rb
    # Manually set the user into the session and auth proxy
    #
    # Parameters:
    #   user - An object that has been setup to serialize into and out of the session.
    #   opts - An options hash.  Use the :scope option to set the scope of the user, set the :store option to false to skip serializing into the session, set the :run_callbacks to false to skip running the callbacks (the default is true).
    #
    # :api: public
    def set_user(user, opts = {})
      scope = (opts[:scope] ||= @config.default_scope)

      # Get the default options from the master configuration for the given scope
      opts = (@config[:scope_defaults][scope] || {}).merge(opts)
      opts[:event] ||= :set_user
      @users[scope] = user

      if opts[:store] != false && opts[:event] != :fetch
        options = env[ENV_SESSION_OPTIONS]
        if options
          if options.frozen?
            env[ENV_SESSION_OPTIONS] = options.merge(:renew => true).freeze
          else
            options[:renew] = true
          end
        end
        session_serializer.store(user, scope)
      end

      run_callbacks = opts.fetch(:run_callbacks, true)
      manager._run_callbacks(:after_set_user, user, self, opts) if run_callbacks

      @users[scope]
    end

あとでつづきかく

10
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
14