Deviseで生成したユーザーの更新フォームに自前のフィールドを追加する
Rails4+Deviseの話。Deviseでユーザーを作るとデフォルトでユーザー情報更新フォームがついてくる。これはbootstrapでスタイル付けしてあるものだけど、基本はこんな感じになる。

このUserモデルに対して別のフィールドをいくつか追加した上で、同じフォームで更新可能にするにはどうしたらいいのか、というのが今日のテーマ。
例えば名前とか、表示名とか、住所とか、そういう情報がアプリケーションに依っては必要になってくると思う。
今回はnameというカラムをUserに持たせることにする。なお、add_column :users, :name, :stringみたいなマイグレーションは既に済んでいるとする。
DeviseのカスタムViewを用意する
% rails generate devise:views
このコマンドによってアプリケーションディレクトリ内にdevise用のビューが生成される。ちなみに先ほどのスクリーンショットは生成したビューを日本語化したもの。
これをやっておくことでDeviseのコントローラが表示に使うビューを自由に変更することができる。
registrations/edit.html.hamlを編集する
erbの人は随時読み替えてもらえばOK。app/views/devise/registrations/edit.html.hamlにname用のフィールドを追加する。
例えばこんな感じで3行ほど追加する。
%h2 アカウント情報更新 = form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :method => :put, role:"form" }) do |f| = devise_error_messages! -# この3行を追加 ################################################## .form-group = f.label :name, "名前" = f.text_field :name, :autofocus => true, class:"form-control" -# ############################################################## .form-group = f.label :email, "Eメール" = f.email_field :email, class:"form-control" - if devise_mapping.confirmable? && resource.pending_reconfirmation? .form-control Currently waiting confirmation for: #{resource.unconfirmed_email} .form-group = f.label :password, "パスワード" %i (パスワードを変更しない場合は空欄のままにしてください) = f.password_field :password, :autocomplete => "off", class:"form-control" .form-group = f.label :password_confirmation, "パスワード(再入力)" = f.password_field :password_confirmation, class:"form-control" .form-group = f.label :current_password, "現在のパスワード" %i (更新するために現在のパスワードが必要です。) = f.password_field :current_password, class:"form-control" .form-group = f.submit "更新", class:"btn btn-primary" %h3 退会 %p 退会してユーザー情報を削除しますか? #{button_to "退会する", registration_path(resource_name), :data => { :confirm => "よろしいですか?" }, :method => :delete, class:"btn btn-danger"} = link_to "戻る", :back
さあこれでフォームができたからもう動くだろうと思ってみると実際にはそうはいかない。Rails4から導入されているStrong Parametersの概念により、許可されていないnameを更新できないというエラーが発生してしまうからだ。
Devise用のコントローラを作る
nameの更新を許可するため、Deviseのコントローラを継承したものを用意する必要がある。このコントローラでは、更新のためのupdateアクションでのみnameパラメータを追加でpermitするという処理を行い、それ以外はベースクラスであるDevise::RegistrationsControllerでDevise従来の処理を行う。
# app/controllers/users/registrations_constroller.rb class Users::RegistrationsController < Devise::RegistrationsController before_action :configure_permitted_parameters, only: [:update] private def configure_permitted_parameters devise_parameter_sanitizer.for(:account_update) do |u| u.permit(:name, :email, :password, :password_confirmation, :current_password) end end end
パラメータの許可はdevise_parameter_sanitizerというDevise用のオブジェクトを経由して行われていて、この部分で:nameも許可対象として指定してあげれば良い。
そしてconfig/routes.rbを編集し、新しく作ったUsers::RegistrationsControllerを使うようにルートを書き換えればよい。
# before devise_for :users # after devise_for :users, controllers: { registrations: 'users/registrations' }
完成!
RailsでDeviseを使ったときのログイン周りの画面遷移
Deviseを使ってユーザー認証機能を作ったときに、もうちょっとよくするための方法メモ。Deviseは普通の実装するとログイン後は/にリダイレクトされる。それを別のURLにする方法はここに書いた。
Deviseでログイン後のURLを変える方法 - PILOG
では、ログインに必要なページのURLに直接アクセスがあったときはどうするか。流れとしてはこうなるのが望ましい。
- ログインが必要なページにアクセスがある
- ユーザーにログインフォームを見せる
- ログインが完了したら最初にアクセスしたURLへリダイレクト
ログインが必要かどうかはコントローラレベルで制御する。これはまあよくやるやつ。
# application_controller.rb class ApplicationController < ActionController::Base private def sign_in_required redirect_to new_user_session_url unless user_signed_in? end end # hoge_controller.rb class HogeController < ApplicationController before_action :sign_in_required def index # ... end def ... end
全てのコントローラで共有するために、ApplicationControllerにsign_in_requiredというメソッドを定義する。これにはログインしていなければログインページヘリダイレクトする、と書かれている。このsign_in_requiredは各コントローラのbefore_actionで使い、指定したアクションを要ログインにすることができる。
ここまでの実装だと1と2までは実現できているが、最初に開こうとしたURLへのリダイレクトは実現されておらず、ユーザーとしては非常に面倒くさい。リダイレクト部分を実現するためには最初のURLを覚えておいて、ログイン後のURLを/ではなく別のURLに差し替えてあげる必要がある。
そこでApplicationControllerをさらに拡張する。
class ApplicationController < ActionController::Base before_action :store_location private def sign_in_required redirect_to new_user_session_url unless user_signed_in? end def store_location return unless request.get? if (request.path != "/users/sign_in" && request.path != "/users/sign_up" && request.path != "/users/password/new" && request.path != "/users/sign_out" && !request.xhr?) session[:previous_url] = request.fullpath end end def after_sign_in_path_for(resource) session[:previous_url] || root_path end end
store_locationでURLを覚えておき、after_sign_in_path_forで覚えておいたURLを返すということをしている。そしてstore_locationを全てのアクションの【前】に呼び出している。
詳しいことは↓に書かれているが、ここの例ではstore_locationをafter_filterに指定、つまり各アクションの後に呼んでいる。今回の例ではアクション中にログインページにリダイレクトされてしまうため、覚えておくURLが適切なものではなくなってしまう。そこでbefore_actionを指定することでなんとか実現している。
Deviseでログイン後のURLを変える方法
RailsでDeviseを使っているときの話。いくらでも情報がある話ですが、自分のためにメモります。
http://hostname/ にログインボタンを置いておいて、ログイン後はhttp://hostname/memberとかに移動したいケースがあると思いますが、これは比較的簡単に書けます。
# application_controller.rb ApplicationController # ... def after_sign_in_path_for(resource) member_path # ログイン後に遷移したいパス end end
応用例
Deviseのドキュメントからの引用です。
after_sign_in_path_forログイン前のパスを覚えておいて、ログイン後にはそちらのURLに戻るという遷移を実現できます。
ありそうなシチュエーションとしては、FacebookとかでシェアされたコンテンツのURLを踏んだ人がサインアップしたらそのコンテンツのURLに飛ぶみたいな場合ですね。
次のようなコードで実現できるようです。要はリクエストのたびにフルパスの情報をセッションに残しておいて、ログイン後のURLは保存しておいた値を使うというものです。
# application_controller.rb after_filter :store_location def store_location # store last url - this is needed for post-login redirect to whatever the user last visited. if (request.fullpath != "/users/sign_in" && request.fullpath != "/users/sign_up" && request.fullpath != "/users/password" && !request.xhr?) # don't store ajax calls session[:previous_url] = request.fullpath end end def after_sign_in_path_for(resource) session[:previous_url] || root_path end
Railsでログインとは別に複数のサービスとの連携を行う方法

ログインはメールアドレスでさせておいてログイン後に各種SSOサービスとの連携を済ませる方法を考えてみます。
まず、お手軽にやりたいのでDeviseとOmniauthを使うのは確定です。omniauth-facebookやomniauth-twitterなどを使うと簡単に連携できますよね。
ところが、よくあるDevise+Omniauthのサンプルを見ると大体ユーザーモデルにOAuthの結果を結びつけていることが多いです。ユーザー1人に対してサービス1種類が関連づけられるみたいな。
でも複数のサービスと接続したいということもありそうです。というか、実際多くのサービスでログインしたあとで他のサービスとの関連付けを行ったりできます。QiitaとかChatworkとか、Gunosyとかもそうだったかも。
モデルを分けます
ユーザーモデルにサービスと認証したフィールドを持たせるからいけないのであって、ユーザーは独立して存在し認証情報は別モデルとしてユーザーと関連づければ、複数のサービスとの連携も可能です。
複数のサービスを区別なく扱える認証モデルみたいなのを作って、ユーザーモデルと1対多の関係をもてばよさそう。
実はまだ試していないので、今から実装しつつ試してみます。
↑の方法でできました
サンプルアプリケーションはこちらにあります。
全体の流れ
- gemを追加
- omniauthの設定ファイルを作る
- Modelを作る
- Controllerを作る
- Viewを作る
- ブラウザで確認!
gemを追加する
Gemfileに必要なGemを追加します。
# Gemfile gem 'devise' gem 'omniauth' gem 'omniauth-hatena' gem 'omniauth-github' gem 'omniauth-twitter' gem 'figaro' gem 'haml-rails’
ユーザー認証のためのDeviseとOAuth連携のためのomniauth各種は当然。ConsumerKey等を隠すためのfigaroとHamlを入れています。
今回はTwitter、Github、はてなとの連携をしてみました。
omniauthの設定ファイルを作る
omniauth用の設定ファイルでConsumerKey等を指定してあげます。Keyはそれぞれ以下のリンクから発行可能。
# config/initializers/omniauth.rb Rails.application.config.middleware.use OmniAuth::Builder do provider :hatena, ENV['HATENA_CONSUMER_KEY'], ENV['HATENA_CONSUMER_SECRET'], { scope: "write_public,read_public,write_private,read_private" } provider :twitter, ENV['TWITTER_CONSUMER_KEY'], ENV['TWITTER_CONSUMER_SECRET'] provider :github, ENV['GITHUB_CONSUMER_KEY'], ENV['GITHUB_CONSUMER_SECRET'] end
Figaroを使っているので、KeyやSecretはconfig/application.ymlに書きます。
# config/application.yml development: HATENA_CONSUMER_KEY: YOUR_HATENA_KEY HATENA_CONSUMER_SECRET: YOUR_HATENA_SECRET TWITTER_CONSUMER_KEY: YOUR_TWITTER_KEY TWITTER_CONSUMER_SECRET: YOUR_TWITTER_SECRET GITHUB_CONSUMER_KEY: YOUR_GITHUB_KEY GITHUB_CONSUMER_SECRET: YOUR_GITHUB_SECRET
Modelを作る
続いて、ユーザーモデルと認証モデルを作成します。
$ rails g devise:install $ rails g devise User # ←コメントで指摘をいただいたので修正しました $ rails g model Auth uid:string provider:string user_id:integer
ユーザーモデルと認証モデルはこんな感じ。ユーザーモデルは複数のAuthモデルと関連することができます。
# user.rb class User < ActiveRecord::Base # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :registerable, :confirmable, :recoverable, :rememberable, :trackable, :validatable has_many :auths end
# auth.rb class Auth < ActiveRecord::Base belongs_to :user end
Controllerを作る
ログインなどのリンクを置くためのルートページ用コントローラと、認証を取り扱うコントローラを作っておきましょう。
$ rails g controller home index $ rails g controller auth create destroy
HomeControllerでユーザーが触るページを作ります。ログイン済みの場合は現在連携中のサービスをArrayで持っておき、あとでViewで使用します。
# home_controller.rb class HomeController < ApplicationController def index if user_signed_in? @providers = current_user.auths.pluck(:provider) end end end
AuthControllerでは認証後のcallback先としてのcreateと、連携解除のためのdestroyメソッドを用意します。
# auth_controller.rb class AuthController < ApplicationController before_filter :authenticate_user! def create auth = request.env["omniauth.auth"] uid = auth["uid"] provider = auth["provider"] unless Auth.find_by_uid_and_provider(uid,provider) Auth.create(uid:uid, provider:provider, user_id:current_user.id) end redirect_to root_url end def destroy provider = params[:provider] auth = Auth.find_by_provider_and_user_id(provider,current_user.id) auth.destroy redirect_to root_url end end
createではauth_hashから認証オブジェクトを生成しユーザーと関連づけ、destroyでは認証オブジェクトを削除します。
routingの設定も必要なので、config/routes.rbは次のようにします。
# config/routes.rb root "home#index" devise_for :users get "/auth/:provider/callback" => "auth#create" delete "/auth/destroy/:provider" => 'auth#destroy', as: :destroy_connection
Viewを作る
最後にViewを作ります。
ログインしていない場合はログインリンクとサインアップリンクを、ログイン済みの場合は各種サービスとの連携/連携解除リンクとログアウトボタンを表示します。
さきほどhome#indexで取っておいた@providerはここで使っています。
%h1 multi auth - if user_signed_in? %ul %li= link_to "ログアウト", destroy_user_session_path, method: :delete - if @providers.include? "twitter" %li twitterと接続済み = link_to "接続解除", destroy_connection_path(:twitter), method: :delete, data:{confirm:"Sure?"} - else %li= link_to "twitterと接続", "/auth/twitter" - if @providers.include? "github" %li githubと接続済み = link_to "接続解除", destroy_connection_path(:github), method: :delete, data:{confirm:"Sure?"} - else %li= link_to "githubと接続", "/auth/github" - if @providers.include? "hatena" %li hatenaと接続済み = link_to "接続解除", destroy_connection_path(:hatena), method: :delete, data:{confirm:"Sure?"} - else %li= link_to "hatenaと接続", "/auth/hatena" - else %ul %li= link_to "ログイン", new_user_session_path %li= link_to "サインアップ", new_registration_path(:user)
ブラウザで確認!
こんな感じになりました。

まとめ
今回作ったAuthモデルには認証によって得ることができる情報うちuidとproviderの2つの情報しか残していませんでしたが、ここにアクセストークンなどをいい感じで置いておくことでサービスのAPIを叩いていろいろな連携を行うことができるようになります。