基礎Ruby on Rails Chapter8 単数リソース

前 [基礎Ruby on Rails Chapter7 バリデーションと国際化](https://qiita.com/tseno/items/e2d83988b6118412acbf)
次 [基礎Ruby on Rails Chapter8 アクション・コールバック／マイアカウントページの作成](https://qiita.com/tseno/items/46065bc132b189c37ccb)


# 単数リソース

## 単数リソースとは

- member等と比較して、単数リソースとは１つしか存在しないもの。
- 例えば「セッション」「自分のアカウント」「自分のパスワード」など、自分自身の情報。

## 単数リソースのルーティング

- ルーティングを設定するには、routes.rbの中で、resourcesメソッドではなく、単数形のresourceを使う。

```rb
resource :account
```

- 集合である、indexアクションはない。
- idパラメータもない。
- membersテーブルの自分自身のレコードのみを使う。

|アクション|パス|HTTPメソッド|パスを示すシンボル|パスを返すメソッド|
|:--|:--|:--|:--|:--|:--|
|show|/account|GET|:account|account_path|
|new|/account/new|GET|:new_account|new_account_path|
|edit|/account/edit|GET|:edit_account|edit_account_path|
|create|/account|POST|:account|account_path|
|update|/account|PATCH|:account|account_path|
|destroy|/account|DELETE|:account|account_path|

# セッションを使ったログイン機能

## セッションとは

### Railsのセッション

- Railsはセッションデータを複合化してクッキーに保存するが、暗号化はしないので、ユーザーがデータを解読できる。
- ただし、改ざんした場合はエラーになる。

### セッションデータへのアクセス

- 「session[:データ名]=値」でセッションに値を入れる。
- 「session[:データ名]」でセッションから値を取り出す。
- 「session.delete(:データ名)」でセッションのデータを消す。
- 「cookie[:データ名]=値」でクッキーに値を入れる。
- 「cookie[:データ名]」でクッキーから値を取り出す。
- cookieの有効期間の設定

```rb
cookies[:name] = { value: "sato", expires: 30.days.from_now }
```

- セッションデータの保存期間
- 自動ログインには、cookieの代わりにcookie.signedを使う。

```rb
cookies.signed[:member_id] = member.id
```

## パスワードの保存

### ハッシュ値とbcrypt

- パスワードをそのまま保存するのではなく、ハッシュ値を保存する。通常Ruby on Railsでは、bcryptを使う。
- Gemパッケージ、bcrypt-rubyを組み込む必要がある。以下のようにコメントを外す。

```rb:Gemfile（一部）
# Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7'
```

- bundle installを実行する。

```sh
$ bundle install
```

### ハッシュ値を保存するカラムの追加

- パスワードのハッシュ値を保存するカラムを追加する。
- マイグレーションスクリプトを作成する。

```sh
$ bin/rails g migration AlterMembers1
```

- マイグレーションスクリプトに以下のように、テーブル名、追加するカラム名、型名を追加する。

```rb:db\migrate\20180927130034_alter_members1.rb
class AlterMembers1 < ActiveRecord::Migration[5.2]
  def change
    add_column :members, :password_digest, :string
  end
end
```

- マイグレーションを実行する。

```sh
$ bin/rails db:migrate
```

### クラスメソッドhas_secure_password

- モデルに、クラスメソッドhas_secure_passwordを追加する。

```rb:app/models/member.rb
class Member < ApplicationRecord
  has_secure_password
```

- これにより、Memberクラスにpasswordおよびpassword_confirmationという名前の2つの属性が定義される。
- password属性に対する次ようなバリデーションが設定される。
    - レコードの挿入時には、パスワードが空ではいけない。
    - パスワードの長さは72文字以下。
    - パスワードと確認用パスワードは一致すること。

- これらのバリデーションを無効にした場合は、以下のようにvalidationオプションをfalseに設定する。

```rb
has_secure_password validation: false
```

- 開発用のシードデータを修正する。

```rb:rb/seeds/development/members.rb
(省略)
    password: "asagao!",
    password_confirmation: "asagao!"
  )
end
```

- データベースをリセットする。

```sh
$ bin/rails db:rebuild
```

## ユーザーの認証

### authenticateメソッド

- `member = Member.first`で1行取得する。
- `member.authenticate("detarame")`で、パスワードが正しいか調べる。間違っているので、false。
- `member.authenticate("asagao!")`は正しいので、1行返ってくる。

```sh
irb(main):003:0> member = Member.first
  Member Load (0.3ms)  SELECT  "members".* FROM "members" ORDER BY "members"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<Member id: 1, number: 10, name: "Taro", full_name: "佐藤 太郎", email: "Taro@example.com", birthday: "1981-12-01", sex: 1, administrator: true, created_at: "2018-09-27 13:16:20", updated_at: "2018-09-27 13:16:20", password_digest: "$2a$10$ShhumBcGJozj0uAYD7WU1.1Ul0bj1YS3l6rLuwLygJV...">
irb(main):004:0> member.authenticate("detarame")
=> false
irb(main):005:0> member.authenticate("asagao!")
=> #<Member id: 1, number: 10, name: "Taro", full_name: "佐藤 太郎", email: "Taro@example.com", birthday: "1981-12-01", sex: 1, administrator: true, created_at: "2018-09-27 13:16:20", updated_at: "2018-09-27 13:16:20", password_digest: "$2a$10$ShhumBcGJozj0uAYD7WU1.1Ul0bj1YS3l6rLuwLygJV...">
irb(main):006:0>
```
### SessionsControllerの作成

- Sessionsコントローラに、create、destroyアクションを追加。

```rb:config/routes.rb（一部）
(省略)
  resource :session, only: [:create, :destroy]
end
```

- sessions_controller.rbの作成。

```sh
$ bin/rails g controller sessions
```

- SessionsControllerの実装

```rb:app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def create
    member = Member.find_by(name: params[:name])
    if member&.authenticate(params[:password])
      session[:member_id] = member.id
    else
      flash.alert = "名前とパスワードが一致しません"
    end
    redirect_to :root
  end

  def destroy
    session.delete(:member_id)
    redirect_to :root
  end
end
```

### current_memberメソッドの定義

- 現在ログインしている情報を取得するメソッドを作成。
- helper_methodで登録することにより、テンプレートの中で使うことができる。

```rb:app/controllers/sessions_controller.rb
class ApplicationController < ActionController::Base
  private def current_member
    Member.find_by(id: session[:member_id]) if session[:member_id]
  end
  helper_method :current_member
end
```

### ログインフォームの実装

- 上部に、`flash.alert`の表示欄を追加。
- 次に、全体をform_tagで囲むように修正。
- ユーザー名、パスワードにname属性を追加。

```erb:app/views/shared/_login_form.html.erb
<h2>ログイン</h2>
<% if flash.alert %><p class="alert"><%= flash.alert %></p><% end %>
<%= form_tag :session, id: "login-form" do %>
    <div>
      <label>ユーザー名：</label>
      <input type="text" name="name">
    </div>
    <div>
      <label>パスワード：</label>
      <input type="password" name="password">
    </div>
    <div>
      <input type="submit" value="ログイン">
    </div>
  </form>
<% end %>
```

- ログインしている場合は、サイドバー上部にログインフォームを表示しないよう`unless current_member`を追加する。

```erb:app/views/shared/_sidebar.html.erb（一部）
<%= render "shared/login_form" unless current_member %>

<h2>最新ニュース</h2>
（省略）
```

- ログイン成功後

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/1e59e973-93f6-e803-8f74-15c9827b2faa.png)

- ログイン失敗

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/6089fe28-1f6e-c3ad-b9eb-bb7887216828.png)

参考
[改訂4版 基礎 Ruby on Rails (IMPRESS KISO SERIES)](https://www.amazon.co.jp/%E6%94%B9%E8%A8%824%E7%89%88-Ruby-Rails-IMPRESS-KISO/dp/429500460X/ref=tmm_pap_swatch_0?_encoding=UTF8&qid=1536666358&sr=1-1)