10章:ユーザーの更新・表示・削除
レポートを確認して、Lighthouse で特定された各リンクに rel="noopener" を追加します。 一般的に、外部リンクを新しいウィンドウまたはタブで開く場合は、必ず rel="noopener" を追加してください。
<a href="https://examplepetstore.com" target="_blank" rel="noopener">...</a>
def update
@user = User.find(params[:id])
if @user.update_attributes(user_params)
# 更新に成功した場合を扱う。
else
render 'edit'
end
end
ここでも、update_attributesを使っているが、updateのエイリアスなので、どっちでも良い。
【補足】
update_attributes → falseを返す
update_attributes! → 例外を投げる
class User < ApplicationRecord
attr_accessor :remember_token
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
# 更新時、パスワードが空でもバリデーションを通すようにする。
# 新規登録時は、has_secure_passwordのお陰で、空での登録は出来ない。
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
- コールバックアクションの設定
メソッド化し、使いまわせそうなものは、カスタムヘルパーとして定義する。
users_controllerで定義すべきは、
- アクセスした先が、そのユーザ自身にしか表示されないか
- ログインしているか(ログインしないと見れないページを切り分ける)
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
before_action :correct_user, only: [:edit, :update]
.
.
.
private
# ログイン済みユーザーかどうか確認
def logged_in_user
unless logged_in?
flash[:danger] = "Please log in."
redirect_to login_url
end
end
# 正しいユーザーかどうか(このユーザーは現在ログイン中のユーザーかどうか)確認
def correct_user
@user = User.find(params[:id])
redirect_to(root_url) unless @user == current_user
end
フレンドリーフォワーディングというのは、ログインが必要なページ(仮にAページとします)に未ログイン状態でアクセスした場合に、ログイン画面に遷移させてログインした後はAページに戻すというもの。
実装するには、リクエスト時点のページをどこかに保存しておき、その場所にリダイレクトさせる必要がある。
module SessionsHelper
.
# 記憶したURL (もしくはデフォルト値) にリダイレクト
def redirect_back_or(default)
redirect_to(session[:forwarding_url] || default)
session.delete(:forwarding_url)
end
# アクセスしようとしたURLを覚えておく
def store_location
session[:forwarding_url] = request.original_url if request.get?
end
end
URLをsession変数の:forwarding_urlキーに格納している。
※ポイントはget通信のみ許可しているところ。
session.delete(:forwarding_url)で、ちゃんとデータを消してあげる。
# ログイン済みユーザーかどうか確認
def logged_in_user
unless logged_in?
store_location
flash[:danger] = "Please log in."
redirect_to login_url
end
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
redirect_back_or user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
サムネイル画像のサイズ調整が可能
# 引数を渡してあげる
module UsersHelper
def gravatar_for(user, options = { size: 80 })
gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}"
image_tag(gravatar_url, alt: user.name, class: "gravatar")
end
end
<% provide(:title, 'All users') %>
<h1>All users</h1>
<ul class="users">
<% @users.each do |user| %>
<li>
# 引数にサイズを指定し、サイズ調整が可能。
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
<% end %>
</ul>
- kaminariでなく、will_paginateを使った、ページネーションの手法
gem 'will_paginate', '3.1.6'
gem 'bootstrap-will_paginate', '1.0.0'
<% provide(:title, 'All users') %>
<h1>All users</h1>
<ul class="users">
<% @users.each do |user| %>
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
<% end %>
</ul>
# view側には、これを追記
<%= will_paginate %>
# デフォルトでは30件を抽出する。
def index
@users = User.paginate(page: params[:page])
end
- 管理者の属性をusersテーブルに追加する
$ rails generate migration add_admin_to_users admin:boolean
class AddAdminToUsers < ActiveRecord::Migration[5.0]
def change
# admin属性のdefault値を明示しておいた方が親切だし無難。
add_column :users, :admin, :boolean, default: false
end
end
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
セキュリティ上、Strong Parameterに、admin属性をpermitしない。
def admin_user
redirect_to(root_url) unless current_user.admin?
end
管理者かどうかのメソッドを定義して置くと便利。
11章:アカウントの有効化
実装までの大まかな流れ
(1) 有効化トークンやダイジェストを関連付けておいた状態で、
(2) 有効化トークンを含めたリンクをユーザーにメールで送信し、
(3) ユーザーがそのリンクをクリックすると有効化できるようにする、
AccountActivationsリソースを作るために、AccountActivationsコントローラを生成
$ rails generate controller AccountActivations
# メールにeditのURLを使い、get通信でactivation_tokenを有効化させる
resources :account_activations, only: [:edit]
- 有効化のメールには一意の有効化トークンが必要だが、セキュリティを考慮し、仮想的な属性を使ってハッシュ化した文字列をデータベースに保存する。
※先にログイン機能を実装した際の方法と似ている。
$ rails generate migration add_activation_to_users activation_digest:string activated:boolean activated_at:datetime
class AddActivationToUsers < ActiveRecord::Migration[5.0]
def change
add_column :users, :activation_digest, :string
add_column :users, :activated, :boolean, default: false
add_column :users, :activated_at, :datetime
end
end
ユーザーが新しい登録を完了するためには必ずアカウントの有効化が必要があるため、
「有効化トークン」・「有効化ダイジェスト」はユーザーオブジェクトが作成される前に作成しておく必要がある。
→before_createコールバックを定義し、作成する。
class User < ApplicationRecord
# 有効化トークンは本質的に仮のものでなければならないので、ここでattr_accessorを使い、定義している。
attr_accessor :remember_token, :activation_token
# 基本的に、コールバックの処理はメソッド参照がベター
before_save :downcase_email
before_create :create_activation_digest
validates :name, presence: true, length: { maximum: 50 }
.
.
.
private
# メールアドレスをすべて小文字にする
def downcase_email
self.email = email.downcase
end
# 有効化トークンとダイジェストを作成および代入する
def create_activation_digest
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
end
end
# コンソールで更新するのも良いが、このファイルで明示しといた方が混乱しないかと考える。
User.create!(name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar",
admin: true,
activated: true,
activated_at: Time.zone.now)
99.times do |n|
name = Faker::Name.name
email = "example-#{n+1}@railstutorial.org"
password = "password"
User.create!(name: name,
email: email,
password: password,
password_confirmation: password,
activated: true,
activated_at: Time.zone.now)
end
$ rails db:migrate:reset
$ rails db:seed
メーラーの作成
「rails generate mailer クラス名 メソッド名」
$ rails generate mailer UserMailer account_activation password_reset
# 世にあるメーラーの送信元に合わせる
class ApplicationMailer < ActionMailer::Base
default from: "noreply@example.com"
layout 'mailer'
end
def account_activation(user)
@user = user
mail to: user.email, subject: "Account activation"
end
ユーザー認証用のリンクについては、「rails routes」で、確認をして実装をしていく。
/account_activations/:id/edit(.:format)
- idについては、new_tokenメソッドで生成されたもの(不規則な文字列)に変換する
- クエリパラメーター(URLの末尾で疑問符「?」に続けてキーと値のペアを記述したもの)でメールアドレスを組み込む
↓
edit_account_activation_url(@user.activation_token, email: @user.email)
このようにして名前付きルートでクエリパラメータを定義すると、Railsが特殊な文字を自動的にエスケープする。(@は%40にエスケープするなど)
あとは、text,htmlそれぞれのメールフォーマットを修正する
<h1>Sample App</h1>
<p>Hi <%= @user.name %>,</p>
<p>
Welcome to the Sample App! Click on the link below to activate your account:
</p>
<%= link_to "Activate", edit_account_activation_url(@user.activation_token,
email: @user.email) %>
こちらでも良い。 (text形式であれば、こちらを使う)
<%= edit_account_activation_url(@user.activation_token, email: @user.email) %>
gem 'letter_opener_web'を使わない、開発環境でのメーラーの確認。
config.action_mailer.raise_delivery_errors = true
config.action_mailer.delivery_method = :test
host = 'localhost:3000' # ホストはドメインに自身の合わせる
config.action_mailer.default_url_options = { host: host, protocol: 'https' }
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview
# Preview this email at
# http://localhost:3000/rails/mailers/user_mailer/account_activation
def account_activation
user = User.first
user.activation_token = User.new_token
UserMailer.account_activation(user)
end
# Preview this email at
# http://localhost:3000/rails/mailers/user_mailer/password_reset
def password_reset
UserMailer.password_reset
end
end
users_controllerのcreateアクションを更新
def create
@user = User.new(user_params)
if @user.save
UserMailer.account_activation(@user).deliver_now
flash[:info] = "Please check your email to activate your account."
redirect_to root_url
else
render 'new'
end
end
メール送信を非同期で処理をしたい場合は、「deliver_later」メソッドを使う。
- 有効化トークンとメールをそれぞれparams[:id]とparams[:email]で参照できる
- sendメソッドの強力きわまる機能です。このメソッドは、渡されたオブジェクトに「メッセージを送る」ことによって、呼び出すメソッドを動的に決めることができる。
authenticated?メソッドをsendメソッドを使って抽象化している
# トークンがダイジェストと一致したらtrueを返す
# Userインスタンスのカラムを引数に指定し、返り値でカラムを返している。
def authenticated?(attribute, token)
digest = send("#{attribute}_digest")
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end
# 渡されたトークンがダイジェストと一致したらtrueを返す
# def authenticated?(remember_token)
# return false if remember_digest.nil?
# BCrypt::Password.new(remember_digest).is_password?(remember_token)
# end
def current_user
if (user_id = session[:user_id])
@current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
user = User.find_by(id: user_id)
if user && user.authenticated?(:remember, cookies[:remember_token])
# if user && user.authenticated?(cookies[:remember_token])
log_in user
@current_user = user
end
end
end
アカウントの有効化
class AccountActivationsController < ApplicationController
def edit
user = User.find_by(email: params[:email])
if user && !user.activated? && user.authenticated?(:activation, params[:id])
user.update_attribute(:activated, true)
user.update_attribute(:activated_at, Time.zone.now)
log_in user
flash[:success] = "Account activated!"
redirect_to user
else
flash[:danger] = "Invalid activation link"
redirect_to root_url
end
end
end
有効でないユーザーがログインを弾く
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
if user.activated?
log_in user
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
redirect_back_or user
else
message = "Account not activated. "
message += "Check your email for the activation link."
flash[:warning] = message
redirect_to root_url
end
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
モデルにメソッドの定義
# アカウントを有効にする
def activate
update_attribute(:activated, true)
update_attribute(:activated_at, Time.zone.now)
end
# 有効化用のメールを送信する
def send_activation_email
UserMailer.account_activation(self).deliver_now
end
# アカウント認証用のメール送信を実装
def create
@user = User.new(user_params)
if @user.save
@user.send_activation_email
flash[:info] = "Please check your email to activate your account."
redirect_to root_url
else
render 'new'
end
end