Ruby
Rails
devise
cancancan
Rails5

devise+cancancanでログイン後のページ遷移をいい感じにする

やりたかったのは以下の二点です。

  1. 権限のないページを開いた時ログイン画面にリダイレクトし、ログイン後、最初に開いたページに戻す。
  2. どこかのページから「ログイン」のリンクを踏んでログインしたら、そのリンクを踏んだ元ページに戻す。

ログイン画面にリダイレクトされた時、元のページに戻す

まずは1の方ですが、権限のないページを踏むとCanCan::AccessDeniedがraiseされるのでそれを捕まえてそこでログインページにリダイレクトさせますが、その時に元ページをSESSIONにしまいます。

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  rescue_from CanCan::AccessDenied do |_exception|
    respond_to do |format|
      format.json { head :forbidden, content_type: 'text/html' }
      format.html do
        # ここで表示しようとしたページのURLをセッションにしまう。
        session[:sign_in_referer] = request.original_url
        if current_user
          # こちら「権限がありません」の様なページを表示。今回の記事とあまり関係ないので無視してください。
          redirect_to access_deny_path
        else
          redirect_to sign_in_path, notice: _('ログインしてください。')
        end
      end
      format.js { head :forbidden, content_type: 'text/html' }
    end
  end

ログイン後の遷移先はafter_sign_in_path_forで返します。これはdeviseの機能。ここで先ほどのsession[:sign_in_referer]を返す様にします。

# app/controllers/application_controller.rb

  def after_sign_in_path_for(_resource_or_scope)
    url = session[:sign_in_referer] || current_root_path
    session.delete(:sign_in_referer)
    url
  end

ここで一つ注意点があります。ログアウト後にCanCan::AccessDeniedが発生するページ(ログインが必要なページ)に遷移させるとそのページがsession[:sign_in_referer]に保存されて、次にログインした時に意図しないページにリダイレクトされてしまう可能性があります。ログアウト後は必ず誰でも閲覧できるページにましょう。

ログアウト後のページ遷移はafter_sign_out_path_forで返すことによって指定できます。これもdeviseの機能です。

 def after_sign_out_path_for(_resource_or_scope)
  # 必ずどのユーザーでも権限のあるページにリダイレクトする。
  root_path

  # 管理画面などではログインページを出してしまうのがいい。
  # sign_in_path
 end

ログイン画面のリンクを踏んだらリンクを踏んだ元ページに戻す

2の方はログインページでリファラーを保存します。

class Users::SessionsController < Devise::SessionsController
  # GET /resource/sign_in
  def new
    # session[:sign_in_referer]がある時はリダイレクトでここにきたのでsuperへ
    return super if session.include?(:sign_in_referer)

    referer = request.referer
    # ブックマークなどリファラーがないときはルートに戻す
    if referer.blank?
      session[:sign_in_referer] = root_url
    else
      session[:sign_in_referer] = referer
    end

    super
  end

ページ遷移の流れによっては、ログイン画面がリファラーに入り、ログイン後ログイン画面にリダイレクトされる可能性がありますが、deviseの実装が、ログイン時ログイン画面を表示するとafter_sign_in_path_forにリダレイクされますので、上記の設定だとサイトのルートにリダイレクトされることになります。

あと細かい点ですが、パスワードの再発行画面passwords_controller#newpasswords_controller#editなんかも、session[:sign_in_referer]に保存されると気持ち悪いので、ログインしていたらリダイレクトする様に実装しました。

class Users::PasswordsController < Devise::PasswordsController
  # GET /resource/password/new
  def new
    return redirect_to root_path if current_user.present?
    super
  end

  # GET /resource/password/edit?reset_password_token=abcdef
  def edit
    return redirect_to root_path if current_user.present?
    super
  end

他にもsession[:sign_in_referer]に入れないほうがいいURLがいくつかあるかもしれませんが、一般的なページ遷移ではこれでなんとなく期待通り動いています。ページによっては、session[:sign_in_referer]にしまう時にチェックして代入しない、という選択肢もあると思います。

何か問題を見つけたら追記しますが、お気付きの点があったらコメントお願いします。