Ruby on Railsの開発において、利用者のサインアップ機能を実装しようとしていろいろ試行錯誤しながらなんとか形になったので、備忘録的に記事にします
まだ、初学者でマイナビ出版の「現場で使える Ruby on Rails5 速習実践ガイド」を元に開発したTaskLeafの利用者管理機能に対して、利用者が自ら自身の情報を登録するところまでの実装です
目次
管理者以外が利用者が自分でユーザー登録する機能を実装したい
前提として、この記事は上記の「現場で使える Ruby on Rails5 速習実践ガイド」内で作成するTask管理アプリケーションをベースにしています
タスク管理アプリケーションの作成は、この手の初学者向けの書籍やサイトなんかを見ると割と一般的な例題のようなので、上記の本をお読みでない方でも、参考になるかもしれません
タスク管理アプリケーション(TaskLeaf)の仕様
以下は、説明を行う上で前提となるTaskLeafの仕様です
- 利用者の登録は、管理者権限を持つユーザーが登録/参照/編集できる
- 利用者は、ログインしていなければどの画面のURLを叩いても、ログイン画面にリダイレクトされる
- ログインしていれば、自身のタスクを一覧が参照でき、タスクの新規作成、編集、削除ができる
- タスクは、利用者に紐付いており他の利用者のタスクは参照出来ない
この中で、利用者の登録を管理者権限を持つユーザーが登録するだけではなく、ログイン画面からサインアップボタンで自らの利用者情報を登録する機能を実装するというのが、この記事の目的です
required_adminをコメントアウトする
以下の作業によって、一般ユーザーであっても自分の利用者情報を新規作成、修正、参照できるようにする必要があるため、user_controllerの最初に加えているrequire_adminを一旦コメントアウトします
app>cotrollers>admin>users_controller.rb
#before_action :require_admin
ちなみに、後ほど復活させるので、削除ではなく一旦コメントアウトにしておきます
Route.rbにsignupのルートを設定する
何はともあれ、サインアップをするためのURLを作成する必要があります
config>routes.rbに以下の一行を追加しました
get '/signup',to: 'admin/users#new'
「Rails.application.routes.draw do」の中ならばどこでも良さそうですが、とりあえず上記の本の記載に従って作成したログイン用のルートの直下に追加しました(ログインは、SessionというViewとControllerで制御されています)
追加した結果は、以下です(後半は省略)
Rails.application.routes.draw do get '/login', to:'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy' get '/signup',to: 'admin/users#new'
ログイン画面にサインアップボタンを追加する
次にログイン画面にサインアップボタンを追加します
このアプリケーションは、ログインしていなければ必ずログイン画面にリダイレクトされる仕様であるため、ログイン画面への実装をすれば、他のページにサインアップを用意する必要はないはずです。
見栄えなどは後で調整することにして、一旦以下のようなイメージの画面としました
ログイン画面への実装であるため、sessionのViewへ以下のコードを最後に追加しました(Bootstrapを導入しているため、ボタンのスタイルはBootstrapのスタイルを適用しています)
app>views>sessions>new.html.slimへの追加コード(slimを利用しています)
br = link_to "Sign Up",signup_path,class: 'btn btn-primary'
上記のroutes.rbに「/signup」と記載しただけで、「signup_path」という名称でリンクが作成されるようです
こういうところ便利
login_requiredをSkipするコードをUserのControllerに追加
ここまでの状態で、アプリケーションを立ち上げて、「Sign Up」ボタンを押しても、ログイン画面へ戻ってきてしまいます
上記のこのアプリケーションの仕様の②で記載されているように、ログインしていなければログイン画面にリダイレクトされるという仕様が効いているためです
ログインしていななければ、ログイン画面にリダイレクトする実装方法
この仕様は、以下の記述で実現されています(本を読んでTaskLeafを実装した方には、内容が重複するため読み飛ばしてください)
まず、アプリケーションの挙動全体の基礎となっているapplication_controller.rbに以下のコードが実装されています
app>controllers>application_controller.rb
class ApplicationControllercurrent_userという関数全てのcontrollerから呼び出し可能になっており、今のログインユーザーの情報が確認できます
またlogin_reuired関数で、もしもcurrent_userがいないならば、強制的にログイン画面にリダイレクトする処理を記述されており、それが「before_action」すなわち全てのaction実施前に処理される仕様になっていますこの記載があるため、個別のControllerでログインの有無を確認する必要がありません
利用者の新規登録のみは、リダイレクト対象から外す
このままでは、上記の通りサインアップで、利用者の登録(User#new)のactionが実行されません
上記の本には、実はこういう時の対応方法として、「skip_before_action」の利用方法が記載されていますこのアプリケーションにおいても、基底となるcontrollerにlogin_reqiredを入れてしまうと、肝心のログイン処理そのものまでログインしていないとできないという本末転倒な事になってしまうため、ログインを司るControllerであるsession_controllerには以下の記述があります(後半は省略)
app>controllers>session_controller.rbclass SessionsControllerこれと同じものをusers_controller.rbにいれれば良さそうです
ただし、利用者の参照/削除/編集処理はログインしていない利用者に使わせるわけには行かないので、skipする対象を絞り込む必要があります
以下のサイトで、skip_before_actionについてif文で対象を絞り込めることがわかりました
問題skip_before_action :hoge, only: :method_a, if: -> { condition }とするとifオプションが無視される。skip_before_actionコールバック...
Railsでskip_before_actionにてifとonly(except)を併用できない - Qiita - Qiitaこの記事中のif文を利用した記述方法で、user#newとuser#createのactionのみをskip対象にするため、以下のような表記をusers_controller.rbに記載しました
app>cotrollers>admin>users_controller.rb
classの定義,コメントアウトした「required_admin」の直後に記載skip_before_action :login_required, if: proc{action_name=="new" || action_name=="create"}newだけでなく、createも対象としているのは、利用者登録画面で入力した情報を保存するときも、まだログイン状態ではないためです
サインアップ後にそのままログインした状態にする
このままだと、今度はサインアップでユーザー情報が登録された後、ログイン画面に戻ってしまいます
(利用者情報の登録後のリダイレクト先が利用者情報の紹介画面になっているが、そのactionはログインしていないと見れないため。)
確かに、先程サインアップ時に登録した利用者情報のメールアドレスとパスワードをここで入力すればログインはできるのですが、さすがに面倒ですそのため、利用者情報を作成したときに、ログインされていないならばログイン状態にする必要があります
この修正は、session_controller.rbのsessionのcreate処理を参考に以下のように実装しましたapp>controllers>admin>users_controller.rb
createの関数のsaveの直後に以下のような一文を入れましたsession[:user_id] = @user.id unless current_usercurrent_user(今現在ログイン中でなければ、nilが返る)がなければ、今登録したばかりのuserのidをsessionに保存します
上記の一文も入れて、users_controller.rbのcreateは以下のような関数になりますdef create @user=User.new(user_params) @user.active=false if @user.save session[:user_id] = @user.id unless current_user redirect_to admin_user_url(@user),notice:"ユーザー「#{@user.name}」を登録しました" else render :new end endとりあえず登録後に、自分の登録した情報が確認できるページへリダイレクトしていますが、サインアップの機能を考えたら直接利用ページ(タスク登録ページ)にリダイレクトしたほうがよいかもしれません。
ちなみに、skip_before_actionの対象にUser情報の確認(show)のアクションは対象外にしていませんが、このリダイレクトのタイミングではログイン状態になっていますので、ユーザー登録画面でサインアップ時に登録されるとまずい項目は非表示にする
サインアップを実装した場合、管理者が登録することを前提にしたページ設計だとまずいです
具体的に言えば、サインアップする一般ユーザーが管理者権限の設定ができるのは、まずいです
そのため、管理者権限の設定をするチェックボックスは、ログインしていて管理者権限を保有するユーザーが新規登録するとき以外は表示しないようにしますまずは、newで新規登録を行う画面を呼び出す前に、ログインしているならば、現在ログインしているユーザーの管理者権限の有無(admin属性でtrue or falseで管理)をViewに渡す処理を記述します
app>controllers>admin>users_controller.rbdef new @user=User.new @admin=current_user.admin if current_user endもともとは、@userだけを渡す処理にしていたのですが、@adminに上記の利用者の管理者権限の有無を渡すことにしました
次にViewも修正します
app>views>admin>users>.nav.justify-content-end -if @admin =link_to '一覧',admin_users_path,class: 'nav-link'「=link_to '一覧'」をcontrollerが引き渡した@adminがtrueの時のみ表示されるように変更します
同様に、編集欄からも管理者権限の付与をするか否かのチェックボックスを削除する必要があります。
この編集欄は、editとnewでformを共有しているので、まずは上記のnewのview内で当該のformを読み込む部分で、@adminを渡す形に変えます=render partial: 'form',locals:{user:@user,admin:@admin}その後、当該のform内でもadminの内容によってチェックボックスの表示可否を制御するために以下のコードを記述しました
-if admin .form-check =f.label :admin, class: 'form-check-label' do =f.check_box :admin, class: 'form-check-input' =f.label :adminこれで、admin権限を保有するユーザーがアクセスしない限り、アドミン権限の有無の設定は行えなくなります
一般利用者が他の利用者情報を参照できなくする
ここまでで、一応サインアップを実装し、自分の情報を登録することができるようになります。
しかし、user_controller.rbのreuired_adminを外しているため、このままだと全ての利用者は、直接URLに手を加えることで、他の利用者の情報も参照、修正できてしまいますそれを防ぐための方策を打っておきます
一覧画面の表示は、required_adminを効かせる
利用者一覧を一般利用者に使わせるわけにはいかないので、最初にコメントアウトしたrequire_adminを復活させます
ただし、対象のアクションは一覧だけにるため、以下のような記載にします
app>controllers>admin>users_controller.rb
class定義の直後before_action :require_admin, only: [:index]なお、require_adminを復活させるにあたり、同じcontrollerの中で定義しているrequire_amdmin関数も修正をする必要があります
これは、もともとの関数がログインされた状態であることを前提で作られているのをskipするロジックがはいってしまっているため、そもそもcurrent_userがnilの場合があるためです
app>controllers>admin>users_controller.rb
private 以下のprivate関数として定義def require_admin if current_user redirect_to root_url unless current_user.admin? else redirect_to root_url end endedit/showで他の利用者の情報漏えいを防ぐ
自分の情報を修正したり、参照するためにedit/showについては、上記のreuire_adminの対象に入れることができません
ただ、このままだとURL内にあるid部分を別の数字をいれることで一般利用者が、他の利用者の情報を見たり更新が出来たりしてしまいますそのため、current_userに管理者権限がない場合、検索対象をcurrent_userにする処理をedit/show各々のaction関数に記述します
app>controllers>admin>users_controller.rb
修正前のedit/showの関数def edit @user=User.find(params[:id]) @admin=current_user.admin if current_user end def show @user=User.find(params[:id]) end修正後のedit/showの関数
def edit if current_user.admin @user=User.find(params[:id]) else @user=User.find(current_user.id) end @admin=current_user.admin if current_user end def show if current_user.admin @user=User.find(params[:id]) else @user=User.find(current_user.id) end endこうすることで、URLを直接修正されたとしても、ログインユーザー本人の情報しか参照できなくなります
実は、更にサインアップ後のメール通知や、パスワード再発行など利用者登録を利用者本人に任せるためのサインアップ方式にすると色々追加で必要になる機能がありそうで、まだ道半ばな感じですが、一旦ここまでの実装を記録に残しておきます
以上 Ruby on Railsで利用者サインアップ機能の実装を行う過程を、「現場で使えるRuby on Rails5 速習実践ガイド」のサンプルプログラムtaskleafをベースに説明しました