Railsチュートリアルで押さえておくべきポイント 10・11章

# 10章：ユーザーの更新・表示・削除

レポートを確認して、Lighthouse で特定された各リンクに rel="noopener" を追加します。 一般的に、外部リンクを新しいウィンドウまたはタブで開く場合は、必ず rel="noopener" を追加してください。

```ruby
<a href="https://examplepetstore.com" target="_blank" rel="noopener">...</a>
```

```ruby:app/controllers/users_controller.rb
  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! → 例外を投げる

***

```ruby:app/models/user.rb
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で定義すべきは、
- アクセスした先が、そのユーザ自身にしか表示されないか
- ログインしているか(ログインしないと見れないページを切り分ける)

```ruby:app/controllers/users_controller.rb
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ページに戻すというもの。
実装するには、リクエスト時点のページをどこかに保存しておき、その場所にリダイレクトさせる必要がある。

```ruby:app/helpers/sessions_helper.rb
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)で、ちゃんとデータを消してあげる。

```ruby:app/controllers/users_controller.rb
# ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        store_location
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end

```

```ruby:app/controllers/sessions_controller.rb
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
```
***

サムネイル画像のサイズ調整が可能

```ruby:app/helpers/users_helper.rb
# 引数を渡してあげる
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
```

```html:app/views/users/index.html.erb
<% 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を使った、ページネーションの手法

```ruby:Gemfile
gem 'will_paginate',           '3.1.6'
gem 'bootstrap-will_paginate', '1.0.0'
```

```html:app/views/users/index.html.erb
<% 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 %>
```

```ruby:app/controllers/users_controller.rb
 # デフォルトでは30件を抽出する。
 def index
    @users = User.paginate(page: params[:page])
 end
```
***

- 管理者の属性をusersテーブルに追加する

```command
$ rails generate migration add_admin_to_users admin:boolean
```

```ruby:migrationファイル
class AddAdminToUsers < ActiveRecord::Migration[5.0]
  def change
 # admin属性のdefault値を明示しておいた方が親切だし無難。
    add_column :users, :admin, :boolean, default: false
  end
end
```

```ruby
 def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
 end

```
**セキュリティ上、Strong Parameterに、admin属性をpermitしない。**


```ruby:app/controllers/users_controller.rb
def admin_user
  redirect_to(root_url) unless current_user.admin?
end
```
管理者かどうかのメソッドを定義して置くと便利。

# 11章：アカウントの有効化
実装までの大まかな流れ
(1) 有効化トークンやダイジェストを関連付けておいた状態で、
(2) 有効化トークンを含めたリンクをユーザーにメールで送信し、
(3) ユーザーがそのリンクをクリックすると有効化できるようにする、

AccountActivationsリソースを作るために、AccountActivationsコントローラを生成

```command:
$ rails generate controller AccountActivations
```
```ruby:routes.rb
# メールにeditのURLを使い、get通信でactivation_tokenを有効化させる
resources :account_activations, only: [:edit]
```
- 有効化のメールには一意の有効化トークンが必要だが、セキュリティを考慮し、仮想的な属性を使ってハッシュ化した文字列をデータベースに保存する。  
※先にログイン機能を実装した際の方法と似ている。

```command:カラムを追加(念のため、activated_atカラムも、ここでは追加している)
$ rails generate migration add_activation_to_users activation_digest:string activated:boolean activated_at:datetime
```

```ruby:migrationファイル(booleanのdefault値を追加する)
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コールバックを定義し、作成する。

```ruby:app/models/user.rb
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
```

```ruby:seedデータ
# コンソールで更新するのも良いが、このファイルで明示しといた方が混乱しないかと考える。
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 クラス名 メソッド名」

```command：メソッドを２つ定義している
$ rails generate mailer UserMailer account_activation password_reset
```

```ruby:メーラーのdefault値を更新
# 世にあるメーラーの送信元に合わせる
class ApplicationMailer < ActionMailer::Base
  default from: "noreply@example.com"
  layout 'mailer'
end
```
```ruby:app/mailers/user_mailer.rb(メール送信先とインスタンス変数で指定)
def account_activation(user)
    @user = user
    mail to: user.email, subject: "Account activation"
  end
```
***

ユーザー認証用のリンクについては、「rails routes」で、確認をして実装をしていく。



```command:  
/account_activations/:id/edit(.:format) 
```
- idについては、new_tokenメソッドで生成されたもの(不規則な文字列)に変換する
- クエリパラメーター(URLの末尾で疑問符「?」に続けてキーと値のペアを記述したもの)でメールアドレスを組み込む
　　　　　　　　↓

```ruby:Userモデルで定義したメソッドを活用している
edit_account_activation_url(@user.activation_token, email: @user.email)
```
このようにして名前付きルートでクエリパラメータを定義すると、Railsが特殊な文字を自動的にエスケープする。(@は%40にエスケープするなど)

あとは、text,htmlそれぞれのメールフォーマットを修正する

```html:app/views/user_mailer/account_activation.html.erb
<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'を使わない、開発環境でのメーラーの確認。

```ruby:config/environments/development.rbを修正＋追記
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' }
```

```ruby:test/mailers/previews/user_mailer_preview.rb(アカウント有効化のプレビューメソッド)
# 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アクションを更新

```ruby:メーラーのメソッド名はviewを合わせる必要あり
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メソッドを使って抽象化している

```ruby:app/models/user.rb
# トークンがダイジェストと一致したら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
```

```ruby:カスタムヘルパーのcurrent_userメソッドを書き換え
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
```
***

アカウントの有効化

```ruby:app/controllers/account_activations_controller.rb
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
```
有効でないユーザーがログインを弾く

```ruby:app/controllers/sessions_controller.rb
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
```
モデルにメソッドの定義

```ruby:app/models/user.rb
# アカウントを有効にする
  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
```

```ruby:app/controllers/users_controller.rb
# アカウント認証用のメール送信を実装
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
```