【Rails】deviseを使わないログイン機能の実装【77日目】


【学習内容】


・ユーザーログイン機能の実装
・アソシエーション他

【ユーザーログイン機能の実装】

(1)Userモデルにadminフラグを追加する

ユーザーが管理者かどうかを表すフラグを追加します。

$bin/rails g migration add_admin_to_users

■db/migrate/XXXXXXXXXXX_add_admin_to_users.rb

class AddAdminToUsers < ActiveRecord::Migration[5.2]
 def change
   add_column :users, :admin, :boolean, default: false, null: false
 end
end

まず、ユーザーに対して管理者権限の有無を設定するために、ユーザー情報のデータベースにboolean型のカラム「admin」というカラムを追加します。

adminはboolean型で設定して、管理者権限がある場合はtrue、ない場合はfalseを返す想定です。

また、nilが入るとエラーの元になるので、defaultでfalse、null許可はfalseにしておきます。


(2)ユーザー管理のためのコントローラを実装する

管理系の機能として「ユーザー管理」を行うので、Admin::UserControllerという名前をつけます。

Adminというモジュールの名前空間の中にUserControllerというクラスを定義します。

Webアプリケーションでは往々にして管理画面と公開画面に大きく別れることが多いです。

そのために使うテクニックが名前空間(namespace)です。下記のようなメリットがあります。

・管理画面は/admin/productsのようなパスでアクセスできる
・公開画面は/productsのようなパスでアクセスできる
・モデルはそのままに管理側のコントローラと公開側のコントローラはディレクトリを分ける

$bin/rails g controller Admin::Users new edit show index

またroutingが意図しない感じになっているので、記述内容を変更します。

■config/routes.rb

Rails.application.routes.draw do
 namespace :admin do
   resources :users
 end
 root to: ‘tasks#index’
 resources :tasks
end

■app/controllers/admin/users_controller

class Admin::Userscontroller < ApplicationController

 def new
   @user = User.new
 end

 def create
   @user = user.new(user_params)
   if @user.save
     redirect_to admin_users_path, notice:”ユーザー「#{@user.name}」を登録しました”
   else
     render :new
   end
 end

 private

 def user_params
   params.require(:user).permit(:name, :email, :admin, :password, :password_confimation)
 end
end


■app/views/admin/users/new.html.slim

h1 ユーザー登録
= form_with model: [:admin, @user], local: true do |f|
 .form-group
 = f.label :name ‘名前’
 = f.text_field :name, class: ‘form-control
.form-group
 = f.label :email, ‘メールアドレス’
 = f.text_field :email, class: ‘form-control
.form-check
 = f.label :admin, class: ‘form-check-labeldo
   = f.check_box :admin, class: ‘form-check-input
   | 管理者権限
.form-group
 = f.label :password, ‘パスワード’
 = f.password_field :password, class: ‘form-control’
.form-group
 = f.label :password_confirmation, ‘パスワード(確認)’
 = f.password_field :password_confirmation, class: ‘form-control’
= f.submit ‘登録する’, class: ‘btn btn-primary’

上記のフォームだけで登録可能になりましたが、不完全なデータをはじくようにUserクラスに検証を足しておきます。

■app/models/user.rb

class User < ApplicationRecord
 has_secure_password

 validates :name, presence: true
 validates :email, presence: true, uniqueness: true
end


(3)最終的には各ファイルのコードは下記のようになります。(確認)

■app/controllers/admin/users_controller

class Admin::Userscontroller < ApplicationController
 def index
   @users = user.all
 end

 def show
   @user = User.find(params[:id])
 end

 def new
   @user = User.new
 end

 def edit
   @user = User.find(params[:id])
 end

 def create
   @user = user.new(user_params)
   if @user.save
     redirect_to admin_users_path, notice:”ユーザー「#{@user.name}」を登録しました”
   else
     render :new
   end
 end

 def update
   @user = User.find(params[:id])
   if @user.update(user_params)
     redirect_to admin_user_path(@user), notice: “ユーザー「#{@user.name}」を更新しました”
   else
     render :new
   end
 end

 def destroy
   @user = User.find(params[:id])
   @user.destroy
   redirect_to admin_user_url, notice: “ユーザー「#{@user.name}」を削除しました”
 end

 private

 def user_params
   params.require(:user).permit(:name, :email, :admin, :password, :password_confimation)
 end
end



■app/views/admin/users/index.html.slim

h1 ユーザー一覧
= link_to ‘新規登録’, new_admin_user_path, class: ‘btn btn-primary
.mb-3
.table.table.table-hover
 thead.thead-default
   tr
     th= User.human_attribute_name(:name)
     th= User.human_attribute_name(:email)
     th= User.human_attribute_name(:admin)
     th= User.human_attribute_name(:created_at)
     th= User.human_attribute_name(:updated_at)
     th
 tbody
-	@users.each do |user|
tr
     td= link_to user.name, [:admin, user]
     td= user.email
     td= user.admin? ? ‘あり’ : ‘なし’
     td= user.created_at
     td= usr.updated_at
     td
       = link_to ‘編集’, edit_admin_user_path(user), class: ‘btn btn-primary mr-3’
       = link_to ‘削除’, [:admin, user], method: :delete, data: { confirm: “ユーザー「#{user.name}」を削除します。よろしいですか?”}, class: ‘btn btn-danger’


■app/views/admin/users/new.html.slim

h1 ユーザー登録
.nav.justify-content-end
 = link_to ‘一覧’, admin_users_path, class: ‘nav-link
= render partial: ‘form’, locals { user: @user }

■app/views/admin/users/edithtml.slim

h1 ユーザーの編集
.nav.justify-content-end
 = link_to ‘一覧’, admin_users_path, class: ‘nav-link
= render partial: ‘form’, locals { user: @user }

■app/view/admin/users/_form.html.slim(新規作成)


- if user.errors.present?
 ul#error_explanation
   - user.errors.full_messages.each do |message|
     li = message
= form_with model: [:admin, user], local: true do |f|
 .form-group
   = f.label :name, ‘名前’
  = f.text_field :name, class: ‘form-control
.form-group
 = f.label :email, ‘メールアドレス’
 = f.text_field :email, class: ‘form-control
.form-check
 = f.label :admin, class: ‘form-check-labeldo
  = f.check_box :admin, class: ‘form-check-input
   | 管理者権限
.form-group
 = f.label :password, ‘パスワード’
 = f.password_field :password, class: ‘form-control’
.form-group
 = f.label :password_confirmation, ‘パスワード(確認)’
 = f.password_field :password_confirmation, class: ‘form-control’
= f.submit ‘登録する’, class: ‘btn btn-primary’

■app/views/admin/users/show.html.slim

h1 ユーザーの詳細
.nav.justify-content-end
 = link_to ‘一覧’, admin_users_path, class: ‘nav-link
table.table.table-hover
 tbody
   tr
     th= User.human_attribute_name(:id)
     td= @user.id
   tr
     th= User.human_attribute_name(:name)
     td=@user.name
   tr
     th= User.human_attribute_name(:email)
     td=@user.email
   tr
     th= User.human_attribute_name(:admin)
     td=@user.admin? ? ‘あり’ : ’なし’
   tr
     th= User.human_attribute_name(:created_at)
     td=@user.created_at
   tr
     th= User.human_attribute_name(:updated_at)
     td=@user.updated_at
=link_to ‘編集’, edit_admin_user_path, class: ‘btn btn-primary mr-3’
=link_to ‘削除’, [:admin, @user], method: :delete, data: { confirm:”ユーザー「#{@user.name}」を削除します。よろしいですか?”  }, class: ‘btn btn-danger’

■config/locales/ja.yml


…
ja:
 activerecord:
   …
   attributes:
     tasks:
   …
     user:
       name: 名前
       email: メールアドレス
       admin: 管理者権限
       password: パスワード
       password_confirmation: パスワード(確認)
       created_at: 登録日時
       updated_at: 更新日時
…

これでユーザー管理機能の基礎となる部分は完成しましたが、/admin/usersにアクセスすれば誰でも機能を利用することができてしまいます。

よって、今後のステップとしてログイン機能の実装⇒あとでユーザー管理機能の利用に時間をかけます。

(4)ログイン機能を実装する

Railsにおいてログイン機能を実装する際には、ログインをする=「セッションというリソースを作る」と捉えて、SessionsControllerという名前でコントローラーを作ることが多いです。

また、今回追加するアクションは「ログインのフォームを表示する(new)」、「フォームから送られてきた情報を元にログインを行う(create)」、「ログアウトを行う(destroy)」の3点です。

(5)ログインのフォームを表示する

コントローラーを作るのと同時にnewアクションとビューも作成します。

$bin/rails g controller Sessions new

ルーティングについて、上記のコマンドを実行すると、’sessions/new’が自動で入ると思いますが、今回は、ログインフォームを表示するアクションのURLは’/login’にしたいので、config/route.rbを編集します。

■config/route.rb

Rails.application.routes.draw do
 get ‘/login’, to: ‘sessions#new’end


■app/views/sessions/new.html.slim

h1 ログイン
= from_with scope: :session, local: true do |f|
 .form-group
   = f.label :email, ‘メールアドレス’
   = f.text_field :email, class: ‘form-control’, id: ‘session_email
 .form-group
   = f.label :password, ‘パスワード’
   = f.password_field :password, class: ‘font-control’, id: ‘session_password
 =f.submit ‘ログインする’, class: ‘btn btn-primary


(6)ログインの実行

ルーティングとコントローラーの作成を行っていきます。

■config/route.rb

Rails.application.routes.draw do
 get ‘/login’, to: ‘sessions#new’
 post ‘/login’, to: ‘sessions#create’end

■app/controllers/sessions_controller.rb

class SessionsController < ApplicationController

 def new
 end

 def create
   user = User.find_by(email: session_params[:email])
   if user&.authenticate(session_params[:password])
     session[:user_id] = user.id
     redirect_to root_path, notice: ‘ログインしました’
   else
     render :new
   end
 end

 private

 def session_params
   params.require(:session).permit(:email, :password)
 end
end

createアクションでは、送られてきたメールアドレスで、ユーザーの検索を行います。
ユーザーが見つかったら、次はパスワードによる認証を行います。

authenticateメソッドは認証のためのメソッドで、引数で受け取ったパスワードをハッシュ化して、その結果がUserオブジェクト内部に保存されているdigestと一致するか調べます。

無事にメールアドレスとパスワードの認証に通った場合は、sessionにuser_idを格納します。

これにより、
誰もログインしていない状態 ⇒ session[:user_id]がnil
誰かがログインしている状態 ⇒ session[:user_id]にログイン中のユーザーのIDが入っている。

ちなみに、「&.」は「ぼっち演算子」といい、俗にいうnilガードの役目を果たします。

ここでは、メールアドレスに対応するユーザーデータが見つからないときに、userの値はnilになるので、authenticateメソッドの部分でエラーがでないようにしています。

IT技術にまつわる実験ノート
https://matt-note.hatenadiary.jp/entry/2018/11/14/083307


(7)ログイン情報の取得を簡単にする

ユーザーがログインしている状態であれば、session[:user_id]にユーザーのIDが格納されている状態なので。ログイン後はセッションが生きている限り下記のコードでユーザーを簡単に取得することができます。

User.find_by(id: session[:user_id])

これをApplicationControllerに「current_user」として名前のメソッドを定義しておくことで全てのコントローラーから利用できるようにします。

また、helper_method定義をすることで全てのビューから使えるようにします。

■app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
 helper_method :current_user
 
 private
 def current_user
   @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
 end
end

このままでは、ログインしていなくてもどの機能も利用できてしまうので、ログインしているときだけ利用できるように制限をかけていきます。

(8)ログアウト機能

ログアウトするには、session[:user_id]の値がnilであることを指します。

session.delete(:user_id)を実行することでたしかにuser_idの情報はnilにすることはできますが、sessionにほかの情報が入っていた場合、それらは削除することができません。

reset_sessionを用いると、セッション内の情報を全て削除することができます。

■config/route.rb

Rails.application.routes.draw do
 get ‘/login’, to: ‘sessions#new’
 post ‘/login’, to: ‘sessions#create’
 delete ‘/logout’ to: ‘sessions#destroy’end

■app/controllers/sessions_controller.rb

Class SessionsController < ApplicationController
 …
 def createend

 def destroy
   reset_sessions
   redirect_to root_path, notice: ‘ログアウトしました’
 end

 private
 …
end

■app/views/layouts/application.html.slim

ここではログアウトのリンクを追加します

doctype html
html
…
 body
   .app-title.navbar.navbar-expand-md.navbar-light.bg-light
     .navbar-brand Taskleaf
     ul.navbar-nav.ml-auto
       - if current_user
        li.nav-item= link_to ‘タスク一覧’, tasks_path, class: ‘nav-link
        li.nav-item= link_to ‘ユーザー一覧’, admin_users_path, class: ‘nav-link
        li.nav-item= link_to ‘ログアウト’, logout_path, method: :delete, class: ‘nav-link
       -else
        li.nav-item= link_to ‘ログイン’, login_path, class: ‘nav-link
     .container
       -if flash.notice.present?
         .alert.alert-success= flash.notice
       = yield

これで見た目の部分は整えることはできました。


しかし、認証機能を追加する場合は、「ログインしていなければ一定の機能を使えなくする」、「ログインしているユーザーは自分のデータしか見られない」といった制限を加えていきます。

(9)ログインしていなければタスク管理を利用できなくする

コントローラーの「フィルタ」という機能を使って、アクションを処理する前後に、任意の処理を挟みます。

今回は、各アクションの前に処理されるフィルタを設置して、ユーザーのログイン状況を調べ、結果に応じて機能を限定するようにします。

そこで使用するメソッドが「login_required」です。

このメソッドをApplicationController内でbefore_actionと共に記述することで全てのアクションを実行する前にユーザーのログイン状況を判定するようになります。

■app/controllers/application_controller.rb

class ApplicationController < ActionController::Base

 helper_method :current_user
 before_action :login_required

 private

 def login_required
   redirect_to login_path unless current_user
 end
end

これで全てのアクションを実行する前にユーザーのログイン状況を確認し、ログイン画面を表示するようになりました。

しかし、現状ではsessionis#newアクション(ログイン画面を表示するためのアクション)もlogin_requiredの対象になっているので、アプリケーションのどのURLをリクエストしても無限にredirectが実行されてしまいます。

それを回避するためにSessionsControllerだけは特別にlogin_requiredの対象から外します。

■app/controllers/session_controller.rb

class SessionController < ApplicationController
 skip_before_action :login_requiredend

特定のコントローラーでskip_before_actionを使用する事で、親クラスなどに定義されているフィルタを回避することができます。

(10)DB上でUserとTaskを紐付ける

アプリケーション上でログインしているユーザーのデータだけを扱うようにするには、tasksテーブルにuser_idというカラムを追加して、タスクを所有しているユーザーのIDが格納されるようにします。

同時にRailsのアソシエーションを組むことでログインしているユーザーに紐づいたTaskデータの登録、一覧、詳細の確認などを行えるような処理が必要です。

$bin/rails g migration AddUserIdToTasks

■db/migrate/XXXXXXXXXXXX_AddUserIdToTasks.rb

class AddUserIdToTasks < ActiveRecord::Migration[5.2]
 def up
   execute ‘DELETE FROM tasks’
   add_reference :tasks, :user, null: false, index: true
 end

 def down
   remove_reference :tasks, :user, index: true
 end
end


executeメソッドは任意のSQLを実行し、その内容は今まで作られたタスクが全て削除されます。

なぜ全てのタスクを削除する必要があるかというと、既存のタスクがある状態で、タスクとユーザーの関連づけを行うと、既存のタスクに紐づくユーザーが決められず、NOT NULL制約に引っかかるからです。

■app/models/user.rb

class User < ApplicationRecord
…
 validates :email, presence: true
 has_many :tasks
end

■app/models/task.rb

class Task < ApplicationRecord
 validates :name, presence: true
 validates :validate_name_not_including_comma

 belongs_to :user
 private
 …
end


(11)ログインしているユーザーのTaskデータの登録

■app/controllers/task_controller.rb

def create
   @task = current_user.tasks.new(task_params)
   if @task.save
     redirect_to @task, notice: “タスク「#{task.name}」を登録しました”
   else
     render :new
   end
 end

アソシエーションを活かして、current_user.tasks~~と記述します。
これによりログインしているユーザーのidをuser_idに入れた状態でTaskデータを登録できるようになりました。

(12)ログインしているユーザーのTaskデータだけを読み出す

アソシエーションを組んだので、これまでの諸々の記述内容を変更していきます。

■app/controllers/tasks_controller.rb

(変更前)
def index
 @tasks = Task.all
end
(変更後)
def index
 @tasks = current_user.tasks
end

show,edit,update,destroyアクション内

(変更前)
Task.find(params[:id])
(変更後)
current_user.tasks.find(params[:id])


(13)管理機能を管理者ユーザーだけに利用させるようにする

『(1)Userモデルにadminフラグを追加する』のパートでユーザー管理機能(admin)を実装しました。

現在は、どのユーザーでもこのユーザー管理機能を使える状態なので、他人のパスワードなどを好きに変更することができてしまいます。

そこで、ユーザー管理機能の修正に入る前に、ログインしているユーザーが管理権限を持っている場合のみ、ユーザー一覧画面へのリンクをメニューに表示させるようにしておきます。

その際に利用できる便利なメソッドが「admin?メソッド」です。

このメソッドはレシーバのUserオブジェクトがユーザー権限を持っているかどうか判定します。

つまり、current_user.admin?で現在ログインしているユーザーが管理権限を持っているかどうか判定することができるということです。

■app/views/layouts/application.html.slim

doctype html
html
 head
 …
 body
 …
   ul.navbar-nav.ml-auto
     - if current_user
       li.nav-item= link_to ‘タスク一覧’, tasks_path, class: ‘nav-link
       - if current_user.admin?
          li.nav-item= link_to ‘ユーザー一覧’, admin_users_path, class: ‘nav-link
       li.nav-item= link_to ‘ログアウト’, logout_path, method: :delete, class: ‘nav-link
     - else
       li.nav-item= link_to ‘ログイン’, login_path, class: ‘nav-link
   .container
    …


これにより、ログインしているユーザーが管理者権限を持っているときのみ、ナビゲーションにユーザー一覧画面へのリンクが生成されました。

■app/controllers/admin/user_controller.rb

class Admin::UsersController < ApplicationController
 before_action :require_admin
…
 private
…
 def require_admin
   redirect_to root_path unless current_user.admin?
 end
end

コントローラーの方でも管理者権限を行使するメソッド内に、.admin?を使用する事で縛りをかけています。

(14)最初の管理者ユーザーをつくる

1人も管理者がいない状態だと、誰もtaskleafにログインできず、ユーザーを作ることもできません。

このような時は、Railsコンソールから管理者を作成します。

> User.create(name: ‘admin’, email: ‘admin@example.com’, password: ‘password’, password_confirmation: ‘password’, admin: true)


【所感】

deviseを使用せずにユーザー管理をしようとすると、ひたすら長くてやること多すぎです。。

ただこの長い長い学習の中で、学び方に対する学びも多かったです。

本を読んで、わからない点はググって、読めるようになって、実際に手を動かすみたいなリズムが出てきたように思えます。

この記事が気に入ったらサポートをしてみませんか?