Webエンジニア目指して#42
どうもギター売りマンです strandbergというメーカーの7弦ギターを何故か2本持っているので片方出品しました。何故でしょうねえ・・・ 手放すのが惜しいので元値からあまり下げてないけど、売れて欲しい、売れて欲しくない、売れて欲しい
そんな感じでやっていきましょう、ProgateのRails VIIIです
目標:ユーザーアカウント機能実装
ログインページの作成
view
<h1>ログイン</h1> <% if @error_message %> <%= @error_message %> <% end %> <%= form_tag("/login") do %> <p>e-mail</p> <input name="email" value="<%= @email %>"> <p>パスワード</p> <input type="password" name="password" value="<%= @password %>"> <input type="submit" value="ログイン"> <% end %>
冒頭の記述でログインに失敗したとき用の@error_message
を表示させるようにする
後は今まで通りフォームを記述
input
のtype属性をpassword
にすることで以下の画像のように入力時に隠れるようになる
パスワードの欄を増やしたので前回と同じようにデータベースにも専用のカラムを増やしておく。また、usersモデルにvalidates:password,{presence:true}
のvalidationを設ける
routing
get 'login' => 'users#login_form' post 'login' => 'users#login'
getとpostは同じURLにしても大丈夫らしい(自作するなら分けるかなあ)
action
def login @user=User.find_by(email: params[:email], password: params[:password]) if @user flash[:notice]="ログインしました" redirect_to("/post/index") else @error_message="メールアドレスかパスワードが間違っています" @email=params[:email] @password=params[:password] render("users/login_form") end end
find_by
でフォームに入力されたemailとpassが一致したレコードを取得し、取得の結果でif文を書く
このようにif 変数
という書き方をすると変数がnilかどうかで分岐してくれるみたい(nilの場合にelseになる)
elseにはviewで言及したログイン失敗メッセージ用の変数とフォームの初期値に代入
結果
ユーザー認証は出来たのでログインページは完成
ログイン処理
先程のユーザー認証はユーザーを特定したが飛んだページはただの投稿一覧なので、「このユーザーでログインしてますよ」という処理を書いていく
session変数
session[:キー名]
の形で使う
この変数はブラウザが記憶してくれるらしく、ユーザーidなどを代入しておけばログイン中のユーザー名表示や権限などの実装ができる
action
先程のユーザー認証actionに、認証が成功したときにそのユーザーの情報をsession変数に代入させる
def login @user=User.find_by(email: params[:email], password: params[:password]) if @user session[:user_id]=@user.id session[:user_name]=@user.name flash[:notice]="ログインしました" redirect_to("/post/index") else @error_message="メールアドレスかパスワードが間違っています" @email=params[:email] @password=params[:password] render("users/login_form") end end
ユーザーidとユーザー名を取得させた。
ユーザー名を後述のviewで使う
ユーザーidは、まあそのうちなんか使い道あるでしょ
application.html
ヘッダーにその時ログインしているユーザーの名前を表示させる
<p>ログイン中のユーザー:<%= session[:user_name] %></p>
結果
右上のログイン中に注目
割愛するが、新規登録の際にもパスワードを取得し、登録完了したらそのままログインできるようにする
ログアウト処理
session変数にnilを代入すればブラウザに記憶されたユーザーがリセットされる
view
ログアウトのlink_to
に`{method:"post"}を書いている。フォームデータのやりとりをする場合以外に、session変数に変更を加える場合もpostでroutingする必要がある
また、先程のif 変数
を利用してsession変数の中身があるかないか、つまりログイン状態かログアウト状態かどうかでヘッダーに表示させる項目を変えている
<% if session[:user_id] %> <div class="header-info"> <p>ログイン中のユーザー:<%= session[:user_name] %></p> </div> <ul class="header-menu"> <li><%= link_to("投稿一覧", "/post/index") %></li> <li><%= link_to("新規投稿", "/post/new") %></li> <li><%= link_to("ユーザー一覧", "/users/index") %></li> <li><%= link_to("ログアウト", "/logout",{method:"post"}) %></li> </ul> <% else %> <ul class="header-menu"> <li><%= link_to("新規登録", "/users/new") %></li> <li><%= link_to("ログイン", "/login") %></li> </ul> <% end %>
routing
post 'logout' => 'users#logout'
action
def logout session[:user_id]=nil session[:user_name]=nil flash[:notice]="ログアウトしました" redirect_to("/login") end
結果
変数にログイン中ユーザーのレコードを代入する
上記までsession変数をid用name用で2つ用意していたが、session変数でユーザーid1つ取得しておけば下記のように同一レコードの他のデータを参照できる
<% current_user=User.find_by(id:session[:user_id]) %> <% if session[:user_id] %> <div class="header-info"> <p>ログイン中のユーザー: <%= link_to(current_user.name,"/users/#{current_user.id}") %></p> </div> -----以下略-----
ただ、この書き方だとユーザーデータを参照する全てのviewでレコード用の変数を定義する必要ができてしまう
そこで、下記のようにcontrollerに対してbefore_action :共通処理のaction
と書くことでそのcontrollerの全てのactionの前に、指定した処理をさせるシステムを利用する
class ApplicationController < ActionController::Base before_action :set_current_user def set_current_user @current_user = User.find_by(id: session[:user_id]) end end
こんな感じでクラスの直下に書く。 ApplicationControllerに書いて全てのviewに対してログイン中のユーザーデータを利用、ということもできる
@current_userで定義したところで、先程viewに書いたcurrent_userを書き替える
<% if @current_user %> <div class="header-info"> <p>ログイン中のユーザー: <%= link_to(@current_user.name,"/users/#{@current_user.id}") %></p> </div> -----以下略-----
ついでにifの条件も書き替えた。ログイン/ログアウトactionで定義していたsession[:user_name]は使わないため削除しておく
アクセス制限
ユーザー認証メソッドを作る
application.controllerに以下のメソッドを追加する
def authenticate_user if @current_user == nil flash[:notice]="ログインが必要です" redirect_to("/login") end end
先程before_actionで仕込んだメソッドによって、先に@current_userが定義されるため使い回しができる
before_actionにonlyを指定する
application.controllerにメソッドを作っただけでは作動しないため、users.controllerに以下のように書く
class UsersController < ApplicationController before_action :authenticate_user,{only: [:index,:show,:edit,:update]} ----以下略----
全てのcontrollerはapplication.controllerを継承しているため、このようにusersでも先程の認証用のメソッドを仕込める
また、before_actionの第2引数に{only: [:メソッド1, :メソッド2]}
のように書くとそれらのメソッドに対してのみbefore_action処理が行われるようになる
これによって、ログインが必要な機能、不要な機能を分けることができる。
例えばログインや新規登録にログインが必要だったら困るため、それらを除外してbefore_action処理を行わせるように書く。
逆に、ログイン済でログインや新規登録の画面は表示させたくないのでログイン済みなら弾くメソッドを用意してbefore_actionで各actionに適用させておく
post.controllerは全てログインを必要とさせたいためonlyを使わずbefore_actionで認証用メソッドを仕込んでおく
結果
他のユーザーにユーザー情報を編集させないようにする
ログイン中ユーザーとユーザー情報ページのidが一致している場合だけ編集と削除を表示させる
<% if @current_user.id == @user.id %> <%= link_to("編集","/users/#{@user.id}/edit") %> <%= link_to("削除","/users/#{@user.id}/delete",{method:"post"}) %> <% end %>
表示は消したがURLに入力すれば編集ページに入れてしまうため、そこもカバーしていく
ログイン中のユーザーidとURLに入力されたidが違うなら弾くメソッドを作成し、 編集ページとactionに対して適用する
before_action :check_incorrect_user, {only: [:edit,:update]} def check_incorrect_user if @current_user.id != params[:id].to_i flash[:notice]="権限がありません" redirect_to("/post/index") end end
params[:id]は文字列として取得してしまうため、いつかしらのパートで使ったto_iメソッドで数値へ変換して比較演算にかける
結果
ということでRails VIIIはおわり! 書きすぎて長引いちゃった ではでは。