Rails 6 でTodoApp作るぜ part3
今回はUserを作ってログインとかするぜ
Userモデル作成
テーブル設計
名称 | カラム名 | データ型 |
---|---|---|
名前 | name | string |
メールアドレス | string | |
パスワード | password_digest | string |
% rails g model user name:string email:string password_digest:string
今回はしっかりnullつけます
[ db/migrate/?????????.create_user.rb ]
class CreateUsers < ActiveRecord::Migration[6.0] def change create_table :users do |t| t.string :name, null: false t.string :email, null: false t.string :password_digest, null: false t.timestamps t.index :email, unique: true end end end
t.index :email, unique: true
は同じメールアドレスのユーザーが複数人保存されないためにつけています。
% rails db:migrate
パスワードをdigestで保存する
まずはbcrypt
というハッシュ関数を提供するgemをbundleします。
bcrypt
はもともとGemfileに記述されています。しかし、なぜかコメントアウトされています。なのでコメントインしてあげてbundleします
[ Gemfile ]
source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '2.6.5' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '~> 6.0.2', '>= 6.0.2.1' # Use sqlite3 as the database for Active Record gem 'sqlite3', '~> 1.4' # Use Puma as the app server gem 'puma', '~> 4.1' # Use SCSS for stylesheets gem 'sass-rails', '>= 6' # Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker gem 'webpacker', '~> 4.0' # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks gem 'turbolinks', '~> 5' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder gem 'jbuilder', '~> 2.7' # Use Redis adapter to run Action Cable in production # gem 'redis', '~> 4.0' # Use Active Model has_secure_password gem 'bcrypt', '~> 3.1.7' <- これ! # Use Active Storage variant # gem 'image_processing', '~> 1.2' # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', '>= 1.4.2', require: false group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] end group :development do # Access an interactive console on exception pages or by calling 'console' anywhere in the code. gem 'web-console', '>= 3.3.0' gem 'listen', '>= 3.0.5', '< 3.2' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' end group :test do # Adds support for Capybara system testing and selenium driver gem 'capybara', '>= 2.15' gem 'selenium-webdriver' # Easy installation and use of web drivers to run system tests with browsers gem 'webdrivers' end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] gem 'slim-rails' gem 'html2slim'
% bundle
次にuser.rbを編集します
[ app/models/user.rb ]
class User < ApplicationRecord has_secure_password validates :name, presence: true validates :email, presence: true, uniqueness: true end
has_secure_password
はbcrypt
がインストールされていないと使用できません。
has_secure_password
を使うと先ほど作成したuserテーブルpassword
とpassword_confirmation
というカラムが作られます。目には見えませんが確実に作られます。
password
に入力された値とpassword_confirmation
に入力された値が等しかった場合はその値をハッシュ化してpassword_digest
に登録されるという仕組みです。
それと、validatesです。 名前やメールアドレスが空のとき、メールアドレスが既に存在するときは保存に失敗してエラーが発生するようにしています。
users_controllerの作成
% rails g controller users index show new edit
routesの設定
users_controllerにもresources
を使い、tasks_controllerと同じく一般的なCRUD機能を持たせます
[ config/routes.rb ]
Rails.application.routes.draw do resources :users root 'tasks#index' get 'tasks/index', to: 'tasks#index' resources :tasks end
controllerの設定
routesで設定したとうり、シンプルなCRUD機能を作ります
[ app/controllers/users_controller.rb ]
class UsersController < ApplicationController def index @users = User.all end def show @user = User.find(params[:id]) end def new @user = User.new end def create @user = User.new(user_params) if @user.save redirect_to users_url, notice: "ユーザー「#{@user.name}」を登録しました。" else render :new end end def edit @user = User.find(params[:id]) end def update @user = User.find(params[:id]) if @user.update(user_params) redirect_to users_url, notice: "ユーザー「#{@user.nsame}」を更新しました。" else render :edit end end def destroy @user = User.find(prams[:id]) @user.destroy redirect_to root, notice: "ユーザー「#{@user.name}」を削除しました。" end private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end end
viewsの作成
まず初めに、コンソールから適当にユーザーを作っておきます。
name | panda |
---|---|
panda@example.com | |
password | password |
password_confirmation | password |
めっちゃ適当!
% rails c Running via Spring preloader in process 60245 Loading development environment (Rails 6.0.2.1) >> user = User.new(name: "panda", email: "panda@example.com", password: "password", password_confirmation: "password") (1.3ms) SELECT sqlite_version(*) => #<User id: nil, name: "panda", email: "panda@example.com", password_digest: [FILTERED], created_at: nil, updated_at: nil> >> user.save (0.1ms) begin transaction User Create (1.8ms) INSERT INTO "users" ("name", "email", "password_digest", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["name", "panda"], ["email", "panda@example.com"], ["password_digest", "$2a$12$UPS/sV/yXgtL1TJ8vStzJuuDRgTvqPPGbPJVkFpT2K6Pp.dnwSZiO"], ["created_at", "2020-01-18 11:37:54.068311"], ["updated_at", "2020-01-18 11:37:54.068311"]] (2.0ms) commit transaction => true
みてもらうとわかりますが、newではpassword: "password", password_confiramtion: "password"
と入力しています。しかし、saveでは、["password_digest", "$2a$12$UPS/sV/yXgtL1TJ8vStzJuuDRgTvqPPGbPJVkFpT2K6Pp.dnwSZiO"]
と保存されています。これがhas_secure_password
とbcrypt
のパワーです。このハッシュ化によりdbを悪者に覗かれても元のパスワードを盗むことは不可能。らしいです。
それでは、viewsを順番に紹介していきます。(taskとほぼ同じです。)
[ users/index.html.slim ]
h1 アカウント一覧 .nav.justify-content-end = link_to '新規登録', new_user_path, class: 'nav-link' table.table thead.thead-default tr th 名前 th 登録日 th 編集日 tbody - @users.each do |user| tr td = link_to user.name, user td = user.created_at td = user.updated_at
ユーザー一覧となるのが嫌いなのでアカウントにしました。この気持ちわかります??
編集と削除のリンクはつけていません。自分のアカウントを他の人に勝手に消されたら困るので
[ users/show.html.slim ]
h1 アカウントの詳細 .nav.justify-content-end = link_to '一覧', users_path, class: 'nav-link' table.table.table-hover tbody tr th 名前 td = @user.name tr th メールアドレス td = @user.email tr th 登録日 td = @user.created_at tr th 更新日 td = @user.updated_at = link_to '編集', edit_user_path, class: 'btn btn-primary mr-3'
詳細ページでは編集だけ残しています。後で、自分のページでしか表示されないようにします。
基本的にtaskで作ったviewページをコピペしてます。
newとeditの前に_form
を作ります。
[ users/_form.html.slim ]
- if user.errors.present? ul#error_explanation - user_errors.full_messages.each do |message| li = message = form_with model: 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-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-success'
form
はtaskと中身が違うのでお気をつけて!
[ users/new.html.slim ]
h1 アカウント新規登録 .nav.justify-content-end = link_to '一覧', users_path, class: 'nav-link' = render partial: 'form', locals: { user: @user }
[ users/edit.html.slim ]
ちょっと変ですが、とりあえずね・・
h1 アカウントの編集 .nav.justify-content-end = link_to '一覧', users_path, class: 'nav-link' = render partial: 'form', locals: { user: @user } = link_to 'アカウント削除', @user[f:id:yukitoku_sw:20200118215007p:plain], method: :delete, data: { confirm: "タスク「#{@user.name}」を削除します。よろしいですか?" }, class: 'btn btn-danger m-3'
headerにusers_path
のリンクを貼っておきます。
[ app/layouts/_header.html.slim ]
ヘッダーの右端にちょこんとリンクがあります・・・
header.mb-3 .app-title.navbar.navbar-expand-md.navbar-dark.bg-dark .navbar-brand TodoApp ul.navbar-nav.ml-auto li.nav-item = link_to 'User', users_path, class: 'nav-link'
ログイン機能を実装する
ControllerをGしてRoutsをdescribeする
[ コンソール ]
% rails g controller sessions new
コントローラーなのでsessions
と複数形です
[ config/routes.rb ]
Rails.application.routes.draw do get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy' resources :users root 'tasks#index' resources :tasks end
簡単に言うと、
getでログインのフォームを表示
postでログイン状態を作る
deleteでログイン状態を消す(ログアウト状態にする)
Viewの作成
今回はviewから作った方がわかりやすいのでviewを先に作ります
ちょっと腹立つ仕様
[ app/views/sessions/new.html.slim ]
h1 ログインする? = form_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: 'form-control', id: 'session_password' = f.submit 'ログイン', class: 'btn btn-success'
formでメールアドレスとパスワードを受け取ってユーザーを判別します
Controllerを実装する
[ 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_url, notice: 'ログインしました' else render :new end end def destroy reset_session redirect_to login_url, notice: 'ログアウトしました' end private def session_params params.require(:session).permit(:email, :password) end end
new
viewを表示するだけなので何も記述しません。 Simple is the Best
create
まず、session_paramsから受け取ったメールアドレスに該当するユーザーを探します。
ユーザーが見つかった場合
その見つけたユーザーとsession_paramsから受け取ったパスワードを持つユーザーが同一なのか判断します。
authenticateはhas_secure__password
に付随するメソッドのことです。
引数で受け取ったパスワードをハッシュ化してUser内部に保存されているpassword_digestと一致するかを調べてくれます。一致していたらそのUserオブジェクトを返します。一致していなければfalseを返します。
user.authenticate(session_params[:password])
とすると、最初のメールアドレス該当者がいなかった場合にuserがnilになります。するとauthenticateメソッドがundefined method `authenticate' for nil:NilClass (NoMethodError)
が発生してしまうので&.
(ボッチ演算子)を使っています。
そして、生き残ったUserがいた場合はSessionに生き残ったUserのidを格納しています。
destroy
reset_session
をすることでsession[:user_id]から自分のuser_idを取り出すこと(ログアウト)ができます。
ログイン情報の取得をメソッド化する
現状、下記コードでログインしているユーザーを取得することができます。
User.find_by(id: session[:user_id])
これを何度も書くのは面倒です。
なので、メソッドとして定義します。
[ 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
application_controller
にcurrent_user
としてhelper_method
に定義しました。
これによりcurrent_user
は全てのビューで利用できます。
ヘッダーにリンクを貼って試してみます。
[ app/views/layouts/_header.html.slim ]
header.mb-3 .app-title.navbar.navbar-expand-md.navbar-dark.bg-dark .navbar-brand TodoApp ul.navbar-nav.ml-auto - if current_user li.nav-item = link_to 'Tasks', tasks_path, class: 'nav-link' li.nav-item = link_to 'Users', users_path, class: 'nav-link' li.nav-item = link_to 'Logout', logout_path, method: :delete, class: 'nav-link' - else li.nav-item = link_to 'Login', login_path, class: 'nav-link'
ログイン前は、ログインしか表示されていない
ログイン後はログイン以外が表示されています。
ログインユーザーしかタスク管理ができない??
ようにします。
アカウントを登録して、ログインしないとタスク管理まで辿り着けなくするってこと!
[ app/controllers/application_controller.rb ]
class ApplicationController < ActionController::Base helper_method :current_user before_action :login_required private def current_user @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id] end def login_required redirect_to login_url unless current_user end end
今回もapplication_controller.rb
に、login_required
ってフィルタを書いていきます。
強制的にログインページに吹っ飛ばす。ログインしていなければな って感じ
しかし、このままだとログインページにもlogin_required
が適用されて永遠に異空間を彷徨うことになってしまいます。
それは避けましょう
[ /sessions_controller.rb ]
class SessionsController < ApplicationController skip_before_action :login_required #追加 def new end (省略) end
sessions_controller
にskip_before_action :login_required
を追加します。
これにより、セッション機能にはlogin_required
が適用されなくなり、無事にログインページにたどり着くことができます。
ログインすれば他のユーザーのデータも扱えるクネ??
これはあかん
ログインしているユーザー "の" タスクのみ管理できるようにする
UserとTaskを紐付ける。
ログインUserに紐づいたTaskを登録する
ログインUserのTaskのみ一覧、詳細、変更を行えるようにする
よし!?
UserとTaskを紐付ける
% rails g migration AddUserIdToTasks
内容を編集
class AddUserIdToTasks < ActiveRecord::Migration[6.0] def change execute 'DELETE FROM tasks;' add_reference :tasks, :user, forign_key: true end end
execで既存のTaskを削除してから、forign_keyをつけます 既存のTaskにはuser_idが設定されていないのでエラーになります。
% rails db:migrate
関連付けも必要です
[ models/user.rb ]
class User < ApplicationRecord has_secure_password validates :name, presence: true validates :email, presence: true, uniqueness: true has_many :tasks # 追加 end
Userは複数のTaskを持っている
[ models/task.rb ]
class Task < ApplicationRecord validates :title, presence: true, length: { maximum: 30 } validates :description, presence: true belongs_to :user # 追加 end
Taskは各Userに所属している
ログインUserに紐づいたTaskを登録する
tasks_controller
のcreate
を修正します
@task = Task.new(task_params)
の部分を
@task = current_user.tasks.new(task_params)
へと修正
これによりtaskにcurrent_userのuser_idが登録されます。 各Task毎にどのUserの所有物なのかわかるってことよ
[ app/controllers/tasks_controller.rb ]
def create @task = current_user.tasks.new(task_params) # 変更箇所 if @task.save redirect_to tasks_url, notice: "タスク「#{@task.title}」を登録しました。" else render :new end end
ログインUserのTaskのみ一覧、詳細、変更を行えるようにする
先ほど使った@task = current_user.tasks
を乱用します
ちなみに、
@tasks = current_user.tasks
は
@tasks = Task.where(user_id: current_user.id)
と同じです。
[ tasks_controller.rb ]
class TasksController < ApplicationController def index @tasks = current_user.tasks end def new @task = Task.new end def create @task = current_user.tasks.new(task_params) if @task.save redirect_to tasks_url, notice: "タスク「#{@task.title}」を登録しました。" else render :new end end def show @task = current_user.tasks.find(params[:id]) end def edit @task = current_user.tasks.find(params[:id]) end def update @task = current_user.tasks.find(params[:id]) if @task.update(task_params) redirect_to tasks_url, notice: "タスク「#{@task.title}」を更新しました!" else render :edit end end def destroy @task = current_user.tasks.find(params[:id]) @task.destroy redirect_to tasks_url, notice: "タスク「#{@task.title}」を削除しました。" end private def task_params params.require(:task).permit(:title, :description) end end
これで自分のタスクは自分でしか操作できないようになりました。
コードをわかりやすくする
ごちゃごちゃ書いたのでちょっと綺麗にしたい
tasks_controllerで乱用した@task = current_user.tasks.find(params[:id])
をまとめる
[ tasks_controller.rb ]
class TasksController < ApplicationController before_action :set_task, only: [:show, :edit, :update, :destroy] def index @tasks = current_user.tasks end def new @task = Task.new end def create @task = current_user.tasks.new(task_params) if @task.save redirect_to tasks_url, notice: "タスク「#{@task.title}」を登録しました。" else render :new end end def show end def edit end def update if @task.update(task_params) redirect_to tasks_url, notice: "タスク「#{@task.title}」を更新しました!" else render :edit end end def destroy @task.destroy redirect_to tasks_url, notice: "タスク「#{@task.title}」を削除しました。" end private def task_params params.require(:task).permit(:title, :description) end def set_task @task = current_user.tasks.find(params[:id]) end end
privateの下にset_task
を定義します。中身はさっき乱用した@task = current_user.tasks.find(params[:id])
これをbefore_action :set_task, only: [:show, :edit, :update, :destroy]
とすることで、指定したアクションの前に@task = current_user.tasks.find(params[:id])
を実行してくれます。なので各アクションからは記述を削除してokです。
ちょっとすっきりしました。
一覧ページでの表示順を設置していなかったのでscopeを使い指定します。
[ models/task.rb ]
class Task < ApplicationRecord validates :title, presence: true, length: { maximum: 30 } validates :description, presence: true belongs_to :user scope :recent, -> { order(created_at: :desc) } # 追加 end
登録日の新しい順に設定しました。 これをコントローラーに伝えます。
[ tasks_contorller.rb ]
def index @tasks = current_user.tasks.recent end
これだけです!
今回はここまで・・・
_次回、機能武装