【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-label’ do
= 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-label’ do
= 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 create
…
end
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_required
…
end
特定のコントローラーで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を使用せずにユーザー管理をしようとすると、ひたすら長くてやること多すぎです。。
ただこの長い長い学習の中で、学び方に対する学びも多かったです。
本を読んで、わからない点はググって、読めるようになって、実際に手を動かすみたいなリズムが出てきたように思えます。
この記事が気に入ったらサポートをしてみませんか?