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にすることで以下の画像のように入力時に隠れるようになる

f:id:misokatsu_sand:20210403183649p:plain

パスワードの欄を増やしたので前回と同じようにデータベースにも専用のカラムを増やしておく。また、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で言及したログイン失敗メッセージ用の変数とフォームの初期値に代入

結果

f:id:misokatsu_sand:20210408010941g:plain

ユーザー認証は出来たのでログインページは完成

ログイン処理

先程のユーザー認証はユーザーを特定したが飛んだページはただの投稿一覧なので、「このユーザーでログインしてますよ」という処理を書いていく

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>

結果

右上のログイン中に注目 f:id:misokatsu_sand:20210408163418g:plain

割愛するが、新規登録の際にもパスワードを取得し、登録完了したらそのままログインできるようにする

ログアウト処理

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

結果

f:id:misokatsu_sand:20210409024541g:plain

変数にログイン中ユーザーのレコードを代入する

上記まで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で認証用メソッドを仕込んでおく

結果

f:id:misokatsu_sand:20210412031400g:plain

他のユーザーにユーザー情報を編集させないようにする

ログイン中ユーザーとユーザー情報ページの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メソッドで数値へ変換して比較演算にかける

結果

f:id:misokatsu_sand:20210412031517g:plain

ということでRails VIIIはおわり! 書きすぎて長引いちゃった ではでは。