Railsログイン機能　備忘録

#はじめに
railsチュートリアルを一周し、勉強のためアプリを作成した際の、ログイン機能の備忘録です。
内容はrailsチュートリアル６章〜８章ででてくる内容がほとんどになります。
#仕様
- ログイン機能にdeviseは使用しない
- railsチュートリアル６章〜８章の内容
- CSSにbootstrap使用
- ruby 3.0.0 Rails 6.0.3.4

usersテーブル

| カラム名 | 型 |
|:-:|:-:|
| id | integer |
| name | string |
| email | string |
| password_digest | string |

integer:整数
string:文字列が255字以内

バリデーション

- nameは空白禁止、50文字以内
- emailは空白禁止、255文字以内、かぶりなし、フォーマットはxxx@xxx.xxx
- passwordは空白禁止、6文字以上

#usersコントローラー、モデル作成
コントローラーは複数形、モデルは単数形で表記（railsの慣習）

コントローラーの作成

```:ターミナル
$ rails generate controller Users  
```
モデルの作成

```:ターミナル
$ rails generate model User name:string email:string
$ rails db:migrate
```
user.rbに以下を追記

```ruby:app/models/user.rb
  validates :name, presence: true, #空白禁止
                   length: { maximum: 50 } #50文字以内
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i #大文字小文字無視のxxx@xxx.xxxの正規表現
  validates :email, presence: true, #空白禁止
                    length: { maximum: 255 }, #255文字以内
                    format: { with: VALID_EMAIL_REGEX }, #フォーマット指定
                    uniqueness:true #一意性
```

#パスワードのハッシュ化
パスワードをそのままDBに保存するのはセキュリティの観点よりNG
そのため、ハッシュ化しDBに保存できる状態にする
ハッシュ化：不可逆性の文字列に変換すること

usersテーブルにpassword_digest（string型）を追加
マイグレーション名は自由に設定可能だが、to_usersにすることで、usersテーブルを参照
（今回のマイグレーション名：add_password_digest_to_users）

```:ターミナル
$ rails generate migration add_password_digest_to_users password_digest:string
$ rails db:migrate
```

ハッシュ関数であるbcryptを使用するため、gemを追加

```ruby:Gemfile
gem 'bcrypt' #追記
```
```:ターミナル
$ bundle install
```

下記3点の機能を持たせるためにuser.rbにhas_secure_password追記

- セキュアにハッシュ化されたパスワードをpassword_digestに保存　（セキュア：安全に）
- 仮想的にpasswordとpassword_confirmationが使える（存在性と値が一致するかのバリデーションも追加）
- authenticateメソッド追加（引数とパスワードの比較　一致→Userオブジェクト　間違い→false）

また、パスワードのバリデーションも追記
※has_secure_passwordがもつバリデーションは新規製作時のみ適応、更新時は不適応のため


```ruby:app/models/user.rb
has_secure_password
validates :password, presence: true, length: { minimum: 6 } #空白禁止かつ6文字以上
```
コンソールにてハッシュ化されているか確認

```:ターミナル
$ rails console --sandbox
user1 = User.create(name: "qqq", email: "q@q.q", password:"password", password_confirmation: "password")
user2 = User.create(name: "www", email: "w@w.w", password:"password", password_confirmation: "password")
user1.password_digest
=> "$2a$12$poMxe7FIiQpZISIO51XanuJgDkwYD45hCKZHt0.M0Qx1vW5jcFYm."
user2.password_digest
=> "$2a$12$Ld2mRJgw3LVItTgfn2k8LuiRPH.RHkV71.wbfzfdxDT2p5pFgvkva"

```
パスワードは同じだが、password_digestの値が違うのは、
簡単に言えば、bcryptが引数＋ランダムな値を元に文字列を生成しているから

#ユーザー情報の表示
/users/1を開くことで、id＝１のユーザー情報（名前、eメール）を画面表示出来るようにする

コンソール上でユーザーの追加(現段階ではユーザー情報が無いため)

```:ターミナル
$ rails console
User.create(name: "テスト太郎", email: "test@test.com", password:"password", password_confirmation: "password")
=> #<User id: 1, name: "テスト太郎", email: "test@test.com", created_at: "2021-02-14 13:11:00", updated_at: "2021-02-14 13:11:00", password_digest: [FILTERED]>
```
RESTに基づくルーティングの作成

```ruby:config/routes.rb
resources :users
# resources :users, only: [:index, :show] ←必要なものだけを指定する場合
# resources :users, expect: [:index, :show] ←不必要なものを指定する場合
```
個人ページの作成、表示はnameとemailのみ

```erb:app/views/users/show.html.erb
名前：<%= @user.name %><br>
Eメール：<%= @user.email %>
```
showコントローラの記入
User.find(params[:id])と記入することで、送られてきたurl（/users/1）よりid=1を取得できる
（resourcesを記入したことで、/users/:idのurlが送られてきた場合、showを実行する設定のため）

```ruby:app/controllers/users_controller.rb
def show
  @user = User.find(params[:id])
end
```
以上で、/users/1にid=1の名前とEメールが表示される

#登録フォーム作成
/users/newを開くことで、新規ユーザー登録が出来る状態にする

新規登録フォームには、新規Userオブジェクトを与えないといけないため
newコントローラで与えれるように記入する

```ruby:app/controllers/users_controller.rb
def new
  @user = User.new
end
```
form_withヘルパーメソッドを用いて、投稿フォームを作成する


```erb:app/views/users/new.html.erb
<div class="row">
  <div class="col-md-6 col-md-offset-3"> <!--12分割の4−９部分で描画-->
    <%= form_with(model: @user, local: true) do |f| %>

    <%= f.label :name, "名前" %>
    <%= f.text_field :name, class: 'form-control' %>

    <%= f.label :email, "Eメール" %>
    <%= f.text_field :email, class: 'form-control' %>

    <%= f.label :password, "パスワード" %>
    <%= f.password_field :password, class: 'form-control' %>

    <%= f.label :password_confirmation, "パスワードの再確認" %>
    <%= f.password_field :password_confirmation, class: 'form-control' %>

    <%= f.submit "ユーザー登録", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>
```

フォームで記入した情報をDBに登録する
user_params：フォームから送られてきた情報以外の情報を受け取らない処理
（外部から隠したいため、private内に記載）

```ruby:app/controllers/users_controller.rb
  def create
    @user = User.new(user_params) #@userにフォームで記入したユーザー情報を与える
    if @user.save #saveの実行
      redirect_to @user #save成功時user_url(@user)にいく
    else
      render 'new'#save失敗時、フォーム画面を再描画する
    end
  end

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

#登録失敗時、フォームにエラー文を日本語で表示させる
各フィールド毎のエラー文を、フィールド近くに表示させる状態にする

日本語化に必要なgemのインストール

```:Gemfile
gem 'rails-i18n'
```

```:ターミナル
$ bundle install
```
config/application.rbに以下を追記

```:config/application.rb
config.i18n.default_locale = :jack_o_lantern: 
```
カラム名はgemを入れただけでは日本語にはならないため、
config/localesにja.ymlを新規作成し、以下を記載

```:ターミナル
$ touch config/locales/ja.yml
```

```:config/locales/ja.yml
ja:
  activerecord:
    attributes:
      user:
        name: 名前
        email: Eメール
        password: パスワード
        password_confirmation: パスワードの再確認
```
new.html.erbにエラーメッセージを追記
(エラーメッセージはテンプレート化　f.objectで@userを取得可能)


```erb:app/views/users/new.html.erb
<%= f.label :name, "名前" %>
<%= render 'shared/error_messages', object: f.object, column: :name %>　<!--追記-->
<%= f.text_field :name, class: 'form-control' %>
```

```erb:app/views/shared/_error_messages.html.erb
<% if object.errors.include?(column) %>
  <span style="color: red;"><%= object.errors.full_messages_for(column).first %></span>
<% end %>
```

#登録成功時、成功したことを表示させる

createアクションにsave成功時のflashを追記する

```ruby:app/controllers/users_controller.rb
  def create
    @user = User.new(user_params)
    if @user.save
      flash[:success] = "新規登録が成功しました"　#追記
      redirect_to @user 
    else
      render 'new'
    end
  end
```
flashメッセージが存在する際場合にどのページでも表示されるように、
application.html.erbに以下を記載

```erb:app/views/layouts/application.html.erb
  <main>
    <div class="container">
      <% flash.each do |message_type, message| %>
        <div class="alert alert-<%= message_type %>"><%= message %></div>
      <% end %>
      <%= yield %>
    </div>
  </main>
```

#ログインページの作成

新たにsessioonsコントローラ、newアクションを生成する

```:コンソール
$ rails generate controller Sessions new
```
ルーティングの追加
ログインフォーム→new　ログイン処理→create　ログアウト処理→destroy

```ruby:config/routes.rb
  get    '/login',   to: 'sessions#new'
  post   '/login',   to: 'sessions#create'
  delete '/logout',  to: 'sessions#destroy'
```
/loginページの作成
sessionはモデルを持たないため、送信先urlとscopeをform_withで与えている

```erb:app/views/sessions/new.html.erb
<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_with(url: login_path, scope: :session, local: true) do |f| %>

    <%= f.label :email, "Eメール" %>
    <%= f.email_field :email, class: 'form-control' %>

    <%= f.label :password, "パスワード" %>
    <%= f.password_field :password, class: 'form-control' %>

    <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>
```
コントローラ記入前に、以下の機能を使い回せるようにSessionsヘルパーに記入
①sessionヘルパーを用いて、渡されたユーザーを与えることでログイン状態にするメソッド
　※sessionを用いると、一時cookiesに暗号化された状態で保存可能
②session[:user_id]にuser.idが保存されたいた場合、そのユーザーを返すメソッド
※1リクエストに２回以上このメソッドが呼び出されたとき、
　データベースの問い合わせが２回以上発生してしまうため、
　インスタンス変数を用いて２回目以降はインスタンス変数の中身を参照されるようにする


```ruby:app/helpers/sessions_helper.rb
module SessionsHelper
  #　渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] =user.id
  end
  #　現在ログインユーザーがいる場合、ユーザーを返す
  def current_user
    if session[:user_id]
      @current_user ||= User.find_by(id: session[:user_id])
     #@current_user = @current_user || User.find_by(id: session[:user_id])と同義
     #find(session[:user_id])では、データがない場合に例外を返してしまう（find_byはnilを返す）
    end
  end
```
コントローラはそのままではヘルパーは使えないため、以下を記述する
（viewファイルで使う場合は記載不要、modelで使いたい場合は記載必要）

```ruby:app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include SessionsHelper
end
```
createコントローラの記入
sessionモデルが無いことよりエラーメッセージが生成されないため、
flash.nowを用いて、ログイン失敗を表現する
（render→flash.now ※現在のリクエストまで残る　redirect_to→flash ※次のリクエストまで残る）

```ruby:app/controllers/sessions_controller.rb
  def create
    user = User.find_by(email: params[:session][:email])
                   #{ session: { password: "xxx", email: "xxx" } }のemailを取得
    if user && user.authenticate(params[:session][:password])
      #ユーザーがデータベースに存在し、パスワードの認証もok時の処理
      log_in user
      redirect_to user
    else
      flash.now[:danger] = 'Eメールとパスワードの組み合わせが無効です'
      render 'new'
    end
  end
```
#ログイン時と非ログイン時で表示を変更する方法
header部リンク先などを、ログインしている状態か否かで表示内容を変更できる状態にする

Sessionsヘルパーに、current_userがnilではない事で、ログイン状態か否かを判断するメソッドを追加

```ruby:app/helpers/sessions_helper.rb
  #　ユーザーがログイン状態か否かの確認
  def logged_in?
    !current_user.nil?
  end
end
```
viewファイルなどにifを用いてlogged_in?を使用し、表示内容を変更

```erb:app/views/layouts/application.html.erb
<% if logged_in? %>
  <li><%= link_to "Log out", logout_path, method: :delete %></li>
<% else %>
  <li><%= link_to "Log in", login_path %></li>
<% end %>
```
#ユーザー登録時にログイン状態にする

Sessionsヘルパーに記入したlog_in(user)をuserコントローラのcreateアクションに追記

```ruby:app/controllers/users_controller.rb
def create
    @user = User.new(user_params)
    if @user.save
      log_in @user #追加
      flash[:success] = "新規登録が成功しました"
      redirect_to @user #user_url(@user)→@userで表現している
    else
      render 'new'
    end
  end
```

#ログアウト処理
session(:user_id)の中身を消すことでログアウトを行える状態にする

Sessionヘルパーにsession(:user_id)をnilにするlog_outメソッドを記載

```ruby:app/helpers/sessions_helper.rb
  def log_out
    session.delete(:user_id)
    @current_user = nil
  end
```
コントローラのdestroyアクションにlog_outを記載
ログアウト後はルートパスへ飛ぶようにする

```ruby:app/controllers/sessions_controller.rb
  def destroy
    log_out
    redirect_to root_url
  end
```