Ruby on Rails と React Native で作る web & モバイルアプリ [webアプリ編]

# イントロダクション

## 目的

なんか web アプリケーションを作って運用していたら、どうやらモバイルアプリ需要が出てきたことが分かってバタバタ後追いでモバイルアプリ対応することってあるじゃないですか。今回は演習式でそのシミュレーションをします。まず、Ruby on Rails で web アプリケーションを作った後に、API を生やして React Native で web アプリケーションと同じような仕様のモバイルアプリケーションを作ります。

今回作るアプリケーションの完成品は以下のリポジトリに置いていますので、React Native 側の演習のみを行いたい方は Ruby on Rails アプリケーションは以下より clone して[デプロイ](https://qiita.com/OgiharaRyo/private/9949e5a2cda5e48060f0#%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4)し、[モバイルアプリ編](https://qiita.com/OgiharaRyo/private/9d16c935bcf769c82bc4) へ進んでください。

|フレームワーク|リポジトリ|
|:---|:---|
|Ruby on Rails (web)|[https://github.com/ogihara-ryo/zone-web](https://github.com/ogihara-ryo/zone-web)|
|React Native (mobile)|[https://github.com/ogihara-ryo/zone-mobile](https://github.com/ogihara-ryo/zone-mobile)|

いきなり言い訳なんですが、Advent Calendar の1枠としてやるには明らかにやりすぎな記事になってしまいました。お詫び申し上げます。Ruby on Rails と React Native のカレンダーで1枠ずつ頂くのではなく1人 Advent Calendar をやれば良かったなぁと12月に入ってから今更少し後悔しています。ただ、私は深く反省しているので、てへぺろ1つで許されることは目に見えています。張り切っていきましょう。

## 今回作るシステム

スーパーウルトラシンプルなサンプルアプリケーションを作ることにしましょう。今日やるべきタスクを雑に登録してひたすら消化するためだけのタスク管理システムを作りましょう。大人の都合で認証機能を設けた方が記事的に映えるのでログイン必須のシステムが良いですね。`/users/:id/tasks` を叩けば誰でもそのユーザーの今日のタスク状況が見られるようにしましょう。

<img width="347" alt="スクリーンショット 2019-12-13 11.25.13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/a2a3bfc9-2ff2-4f49-14c3-99a6c07b19b9.png"><img width="400" alt="スクリーンショット 2019-12-13 11.26.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/90061325-e98f-d2a0-c566-3ec6b932ddb5.png">

ユースケースとしては、まず各人は朝に今日中に片付けたいタスクを雑に列挙して(まあ朝起きられないおまえらは昼かもしれませんが)、各タスクが終わるたびにチェックを付けていき、プロジェクトマネージャーだか上司だかは URL を共有してもらっているので各人のタスク状況を web から確認できる、みたいな感じにしますか。~~それ Google スプレッドシートでできるよ。~~

### web アプリケーションでできること

- サインアップ
- ログイン
- タスクの CRUD
- 他人のタスクも閲覧のみ可能

### モバイルアプリケーションでできること

- ログイン (サインアップは web でやれ)
- 自身のタスクの CRUD (他人のタスクは web で見ろ)


## 想定読者

記事を書いた動機は、最近 Ruby on Rails で作った web アプリを React Native アプリ対応しようと思ったら参考日本語記事が少なくて苦労したためですが、この記事としては Ruby on Rails の心得が少しだけある初学者レベルを想定して書いています。Ruby on Rails 自体が初めての方も、写経で雑に理解しながら進めていくことはできるようには少しだけ配慮していますが、あまりにも初歩的で詳細すぎる説明は避けるようにも配慮しています。本記事(webアプリ編)だけでも、ちょっとした Ruby on Rails アプリケーションを作ることができるので、初学者の方は挑戦してみてほしいなぁ...という下心があります。

もし、この記事を進めていく上で躓いた場合や理解できないことがあった場合は [@OgiharaRyo](https://twitter.com/OgiharaRyo) までご連絡頂くか、現在300人ほど参加している技術質問 slack コミュ二ティを運営していますので、こちらで質問して頂ければと思います。([slack 招待リンク](https://join.slack.com/t/it-newbie/shared_invite/enQtODQ3MTE0MjM0MzM3LTI2ZmM2ODMzYmViYzY3ODBlM2VhZGRhYWE1NTc0ZmFkZTI2OWIwYmE5MTRlYWU5NTY3OTNjYjJhMTlkNmVhNDI))

## 環境

2019年12月初旬執筆時点の最新版を使用します。

```zsh
% ruby -v
ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-darwin19]

% rails -v
Rails 6.0.1

% psql -V
psql (PostgreSQL) 12.1
```

## 謝辞

以下のツイートで記事のテストプレイヤーを募集したところ、[にしこ](https://twitter.com/pandeiro245)さんと[nono](https://twitter.com/nono51744153)さんにお声がけ頂き、記事中の不備や手順の漏れを確認及び指摘して頂きました。ありがとうございました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">[ゆるぼ]<br><br>Ruby on Rails と React Native でそれぞれサンプルアプリケーションを写経で作る記事を書いているのですが、その Ruby on Rails 編を明日明後日でテストプレイしてくださる方はいらっしゃいませんか？ (続)</p>&mdash; Ryo (@OgiharaRyo) <a href="https://twitter.com/OgiharaRyo/status/1202948964695150593?ref_src=twsrc%5Etfw">December 6, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# Ruby on Rails アプリケーションの開発

## rails new

オプション欲張りセットの `rails new` を実行します。本記事では web アプリケーションを Heroku で公開するため、データベースに PostgreSQL を指定しています。その他のたっぷりあるオプションは今回の記事に関係のないものを全て排除しています。ちなみにアプリケーション名に `zone` と入っているのは、集中して没頭している意識状態になることを「ゾーンに入る」と表現することがあるので、そこから拝借しました。日次でタスクを可視化して消化するだけで、結構集中状態に入りやすくなる気がしませんか？まあそんなことは良いので早く `rails new` してください。Ruby on Rails アプリケーション開発の始まりです！

```zsh
% rails new zone-web --database=postgresql --skip-action-mailer --skip-action-text --skip-active-storage --skip-action-cable --skip-javascript --skip-turbolinks --skip-test
```

データベースを作ってから一応起動を確認しておきましょう。

```zsh
% cd zone-web
% rails db:create
% rails s
```

環境にもよりますが、大人の都合で以下よりあなたの開発サーバーが [localhost:3000](http://localhost:3000) であることを前提にリンクを記述していきますので異なる方は適時読み替えてください。ということで、[localhost:3000](http://localhost:3000) にアクセスしてご機嫌な表示がされたら ok です。

<img width="400" alt="スクリーンショット 2019-11-30 18.55.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/9d52b715-ecfe-8c79-22c4-ca21b27a02b3.png">


## モデリング

とりあえず `User` モデルと `Task` モデルを作成します。

### User

|列|型|説明|
|:---|:---|:---|
|name|string|ユーザーの名前を保存します。|
|account_id|string|今回はメール認証等は行わないので、とりあえず任意のアカウントID(文字列)とパスワードのペアでログインできるようにします。ここをメールアドレスにしてメール認証のプロセスを挟んでも良いでしょう。|
|password_digest|string|ハッシュ化されたパスワードを保存します。このパスワードは復号できません。つまり、ユーザーがどんなパスワードを入力したのかはシステムが後から知ることはできません。|

### Task

|列|型|説明|
|:---|:---|:---|
|user_id|string|どのユーザーのタスクであるのかを保存します。|
|name|string|タスクの名前を入力します。見積もり時間も詳細も不要です。ただ1行完結なタスク内容が書ければ良いのです。日次でタスクを表示するので作成日も必要ですが、 `rails g(enerate)` で生成されたマイグレーションファイルはデフォルトで `created_at` という列にレコードの作成日時が入るようになるのでこれを利用します。ちなみにこれはあまり良い方法ではないので実務では `created_at` を業務上の都合と結び付けないようにしましょう。|

scaffold 等は行わずに Controller と View は温もりのあるお手製で作ります。モデルとマイグレーションファイルは冷たく Generator に頼ります。

```
% rails g model User name:string account_id:string password_digest:string
% rails g model Task user:belongs_to name:string finished:boolean
% rails db:migrate
```

migrate した結果はこのようになっているはずです。

```rb:db/schema.rb
  create_table "tasks", force: :cascade do |t|
    t.bigint "user_id", null: false
    t.string "name"
    t.boolean "finished"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["user_id"], name: "index_tasks_on_user_id"
  end

  create_table "users", force: :cascade do |t|
    t.string "name"
    t.string "account_id"
    t.string "password_digest"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  add_foreign_key "tasks", "users"
```

`Task` モデルには generator が `belongs_to :user` を追加してくれていますが、`User` 側が `Task` を複数持つことはモデルが知らないので一文追加しておきましょう。

```diff:app/models/user.rb
  class User < ApplicationRecord
+   has_many :tasks
  end
```

## サインアップ

とりあえず `User` を作る機能を実装しなければ始まらないので、サインアップ機能を提供しましょう。

### サインアップフォーム表示

#### ルーティング

`/signup` という URL にアクセスされたら `UsersController` の `new` アクションが呼ばれるようにします。また、サインアップの submit で `/signup` という URL に POST された時に `UsersController` の `create` アクションが呼ばれるようにします。

```diff:config/routes.rb
  Rails.application.routes.draw do
+   get 'signup', to: 'users#new'
+   post 'signup', to: 'users#create'
  end
```

#### Controller

新しく `app/controllers/users_controller.rb` を作成し、以下のコードを仮置きします。今回は `new` アクションでフォームが表示されることだけを目的とするので、`create` アクションはまだ書きません。

```rb:app/controllers/users_controller.rb
class UsersController < ApplicationController
  def new
    @user = User.new
  end
end
```

#### パスワードハッシュ化の準備

Gemfile でデフォルトでコメントアウトされている `bcrypt` をコメントインします。

```diff:Gemfile
- # gem 'bcrypt', '~> 3.1.7'
+ gem 'bcrypt', '~> 3.1.7'
```

`bundle install` します。ちなみに `bundle` だけ叩くと `bundle install` されます。

```zsh
% bundle
```

`User` モデルで `has_secure_password` をコールします。今インストールした `bcrypt` の力で、これをコールしておくことでモデルの属性に `password` と `password_confirmation(確認入力用)` が追加され、合致して `save` した時に自動で `password_digest` にハッシュ化されたパスワードがセットされます。

```diff:app/models/user.rb
  class User < ApplicationRecord
+   has_secure_password
  end
```

#### View

新しく `app/views/users/new.html.erb` を作成し、ユーザー情報の入力フォームをマークアップしていきましょう。

```erb:app/views/users/new.html.erb
<%= form_with model: @user, url: signup_path, local: true do |f| %>
  <p>
    <%= f.label :name, 'お名前' %>
    <%= f.text_field :name %>
  </p>

  <p>
    <%= f.label :account_id, 'アカウントID' %>
    <%= f.text_field :account_id %>
  </p>

  <p>
    <%= f.label :password, 'パスワード' %>
    <%= f.password_field :password %>
  </p>

  <p>
    <%= f.label :password_confirmation, 'パスワード(確認)' %>
    <%= f.password_field :password_confirmation %>
  </p>

  <p><%= f.submit '送信' %></p>
<% end %>
```

これで [localhost:3000/signup](http://localhost:3000/signup) にアクセスするとフォームが表示されます。
<img width="288" alt="スクリーンショット 2019-12-01 18.56.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/1b1e5443-26c2-b99d-f130-dc32b499607f.png">

### サインアップ処理実装

今の状態で submit すると(送信ボタンを押すと) 「`create` アクションが見つからないんだが？！」というエラーで怒られます。初学者のあなたは一度 submit してみてエラー画面を目に焼き付けておきましょう。エラーメッセージを目に焼き付けた数が経験値になります。さて、早速 Controller に `create` アクションを実装していきましょう。

```diff:app/controllers/users_controller.rb
  class UsersController < ApplicationController
    def new
      @user = User.new
    end
+
+   def create
+     @user = User.new(user_params)
+     render :new unless @user.save
+   end
+
+   private
+
+   def user_params
+     params.require(:user).permit(:name, :account_id, :password, :password_confirmation)
+   end
  end
```
`user_params` メソッドの中身は Strong Parameters といい、ブラウザーから POST されるはずのパラメーターのキーを個別に許可しています。これは、`id` 等といったサインアップフォームからは設定してほしくない属性(フォームにフィールドを置いていない属性)を特殊な方法で POST されてもデータベースに保存しないための防御機構です。ここで許可した属性以外は、`users` テーブルにセットしないようにしているわけですね。

続いて、作成後に「ご登録ありがとうございます。」とのテキストを表示できるようにします。新しく`app/views/users/create.html.erb` を作ってマークアップします。

```erb:app/views/users/create.html.erb
<p>ご登録ありがとうございます。</p>
```

さて、実装できたら以下のような情報を入力してテストアカウントを作成してみましょう。

|項目|入力する情報|
|:---|:---|
|お名前|テストアカウント|
|アカウントID|test|
|パスワード|password|
|パスワード(確認)|password|

<img width="293" alt="スクリーンショット 2019-12-01 20.57.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/74791513-bac8-42eb-08a2-f76db9b3388f.png">

送信すると、先ほどの `create.html.erb` にマークアップしたテキストが表示されるはずです。

<img width="301" alt="スクリーンショット 2019-12-01 21.09.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/9b78a103-40a1-a42e-52af-faca465f10b5.png">

上手く実装できていれば、`users` テーブルに1レコード作成されているはずです。`rails console` から見てみましょう。

```zsh
% rails c
Running via Spring preloader in process 48857
Loading development environment (Rails 6.0.1)
irb(main):001:0>
```

一番最近作られた `User` を参照します。このように先ほど作ったユーザーが表示されれば ok です。

```zsh
irb(main):001:0> User.last
  User Load (5.6ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]]
=> #<User id: 1, name: "テストアカウント", account_id: "test", password_digest: [FILTERED], created_at: "2019-12-01 11:58:46", updated_at: "2019-12-01 11:58:46">
```

「ご登録ありがとうございます。」との表示を確認したのに `User` の情報が表示されず `nil` が返ってくる場合はコードを見直してみてください。Strong Parameters あたりが怪しいと思います。

### 異常処理

今のサインアップ機構には以下の問題点があります。

1. 名前やアカウントIDやパスワードが空でも登録できてしまう
2. パスワードとパスワード(確認)が違っている場合にユーザーに何も通知されない
3. 同一のアカウントIDを登録できてしまう

同一のアカウントIDを登録できてしまうと、ログインする時に入力されたアカウントIDでどのユーザーで認証すれか良いか分からなくなるので、アカウントIDは既に登録されている値を使えなくする必要があります。これから、それらの問題を解決していきます。

#### サーバーサイドバリデーション

まず、Model 側でバリデーションを設定してレコードを作成する時に値の有効性を検証することにしましょう。パスワードを必須であるかや、パスワードとパスワード(確認)が一致しているかは、`has_secure_password` が検証してくれるので、残りの名前とアカウントIDに関するバリデーションを定義していきます。

```diff:app/models/user.rb
  class User < ApplicationRecord
    has_secure_password
+
+   validates :name, presence: true
+   validates :account_id, presence: true, uniqueness: true
  end
```

これで、名前が必須となり、アカウントIDが必須かつ重複不可となりました。`rails console` で試してみましょう。名前を空にして、アカウントIDを先ほど画面から作った `test` にして、パスワードとパスワード(確認)に別の文字列を入れてみます。

```zsh
irb(main):001:0> User.create!(name: '', account_id: 'test', password: 'password', password_confirmation: 'invalid')
   (0.1ms)  BEGIN
  User Exists? (0.2ms)  SELECT 1 AS one FROM "users" WHERE "users"."account_id" = $1 LIMIT $2  [["account_id", "test"], ["LIMIT", 1]]
   (0.3ms)  ROLLBACK
Traceback (most recent call last):
        1: from (irb):2
ActiveRecord::RecordInvalid (Validation failed: Password confirmation doesn't match Password, Name can't be blank, Account has already been taken)
```

今回重要なのは最終行です。

> ActiveRecord::RecordInvalid (Validation failed: Password confirmation doesn't match Password, Name can't be blank, Account has already been taken)

と書かれていますね。つまり、パスワード(確認)がパスワードと合っていないよ、名前は空にできないよ、アカウントが重複しているよ、と言われています。問題なさそうですね。ちなみに勿論、データベースには保存されていません。不安であれば再び `User.last` で確かめてみましょう。ちなみに、エラーになる条件は他にもパターンがあります。アカウントIDやパスワードが空の場合です。勿論このままそういったパターンの検証を続けても良いですが、実際の開発ではこういった挙動確認はテストコードを書いて行うことが多いです。ただし、テストの話をすると私がうるさくなるので、本記事ではノーテストでいきます。「ノーテストで開発するのはノーヘルでバイクに乗るようなものです。つまり気持ちいいってことです。」みたいなことを誰か偉い人が言っていたような気がします。わかる。

#### エラーメッセージの通知

さて、サーバーからバリデーションエラーで弾かれたことをユーザーに通知してやる必要があります。エラーメッセージは、`@user.errors.full_messages` で配列で取り出すことができます。View の `form_with` ブロックの一番上にエラーメッセージ表示用のマークアップを行いましょう。

```diff:app/views/users/new.html.erb
  <%= form_with model: @user, url: signup_path, local: true do |f| %>
+   <% if @user.errors.any? %>
+     <ul>
+       <% @user.errors.full_messages.each do |message| %>
+         <li><%= message %></li>
+       <% end %>
+     </ul>
+   <% end %>
    ...
  <% end %>
```

これで、`@user` インスタンスがエラー情報を持っている場合のみバリデーションエラーのリストを表示することができます。早速動かしてみましょう。先ほど `rails c` で試してみたことをブラウザーから試してみましょう。以下のスクリーンショットのように、名前を空にして、アカウントIDを先ほど画面から作った `test` にして、パスワードとパスワード(確認)に別の文字列を入れてみます。

<img width="302" alt="スクリーンショット 2019-12-02 2.08.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/db81f90b-a2e2-90b4-21b3-aed5e5ff67b7.png">

この状態で送信ボタンを押して submit すると以下のようにエラーメッセージが表示されます。先ほど `rails c` で確認したエラーメッセージと同様ですね。

<img width="374" alt="スクリーンショット 2019-12-02 2.10.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/160697dc-eddf-d0b4-1f22-f5b8ecc69b1a.png">

ちなみにこのメッセージを日本語にすることもできるのですが、少しコードが散らかって今回作りたいシステムの目的から脱線するので今回は英語のままにしておきます。変なところで投げっぱなし、それがこの記事の雑さです。また、上記2つのスクリーンショットを見比べてみると、ラベルと入力フィールドの間の改行状態が変わっていることが分かります。これは Ruby on Rails (`f.text_field`)が気を利かせて生成される HTML にエラーであることが分かるような手を加えてくれているのですが、ここで詳しくは解説しません。気になる方は、ブラウザーのデベロッパーツールから生成された HTML のソースコードを読んでみてください。変なところで投げっぱなし、それがこの記事の雑さです。

#### クライアントバリデーション

さて、ついでにクライアント(ブラウザー)側でも簡易的なバリデーションを設定しておきましょう。今回はとても簡単な「フィールドを必須にする」という実装だけを行います。なんと、HTML5 の `input` 要素は `required` 属性を与えるだけでそのフィールドが必須であることをブラウザーに指示してくれます。Ruby のフォームビルダーのメソッド(`f.text_field` 等)で `required` 属性を指定するには `required: true` というオプションを与えてやるだけで ok です。 

```diff:app/views/users/new.html.erb
    <p>
      <%= f.label :name, 'お名前' %>
-     <%= f.text_field :name %>
+     <%= f.text_field :name, required: true %>
    </p>

    <p>
      <%= f.label :account_id, 'アカウントID' %>
-     <%= f.text_field :account_id %>
+     <%= f.text_field :account_id, required: true %>
    </p>

    <p>
      <%= f.label :password, 'パスワード' %>
-     <%= f.password_field :password %>
+     <%= f.password_field :password, required: true %>
    </p>

    <p>
      <%= f.label :password_confirmation, 'パスワード(確認)' %>
-     <%= f.password_field :password_confirmation, required: true %>
+     <%= f.password_field :password_confirmation, required: true %>
    </p>
```

この状態でフィールドを空にして submit しようとすると、ブラウザーから以下のスクリーンショットのように警告が表示されてキャンセルされると思います。このスクリーンショットは macOS の Chrome です。

<img width="305" alt="スクリーンショット 2019-12-02 2.57.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/36ed0860-00c9-0484-42aa-dd414eb6d908.png">


「どの道サーバーで弾かれてエラーメッセージも表示されるんだからクライアント側のバリデーションは冗長では？」と思う方もいらっしゃるかもしれませんが、サーバーに問い合わせなくても入力段階で弾けるものは弾いてしまっておいた方が、ユーザーの手間を煩わせずに済むことが多いため、UX の観点からクライアント側でもバリデーションを行うことが多いです。変なところでこだわる、それがこの記事の雑さです。


## スタイリング

無事にサインアップの機能を作ることができましたが、フォームがあまりにも簡素で気分が上がらないので少しだけ見た目に気を使うことにしましょう。ただ、この記事では「CSS 絶対に書きたくないでござる！」という気持ちと、「CSS フレームワークに依存した class 付けは絶対にしたくないでござる！」という気持ちと、「CSS フレームワークの導入は CDN だけで終わらせたいでござる！」という気持ちが強いので、CSS を書かなくても class を付けなくても良くて CDN で読み込むだけでそれっぽい見た目になる方法を探します。そこで [mini.css](https://minicss.org/) を利用します。記事執筆時点で、ここまでに書いたサインアップフォームを一切触らなくても CDN へのリンクを書くだけでスタイルが当たるものを探し回ったところ mini.css が一番それっぽくなったような気がしました。ということで、早速 CDN から mini.css を読み込みます。たった1行書くだけで CSS を丸ごと配信してもらえるなんて至れり尽くせりですね。また、mini.css の [ドキュメント(Getting started)](https://minicss.org/docs) を見ると、「viewport はちゃんと指定しておけよ」と書いてあるので従います。各ページ共通のレイアウト(`head` 要素等) は `app/views/layouts/application.html.erb` に書かれているので、ここに viewport の指定と CDN へのリンクを設定します。

```diff:app/views/layouts/application.html.erb
    <head>
      <title>ZoneWeb</title>
      <%= csrf_meta_tags %>
      <%= csp_meta_tag %>
+     <meta name="viewport" content="width=device-width, initial-scale=1">
+
+     <link rel="stylesheet" href="https://cdn.rawgit.com/Chalarangelo/mini.css/v3.0.1/dist/mini-default.min.css">
      <%= stylesheet_link_tag    'application', media: 'all' %>
    </head>
```

この状態でサインアップフォームにアクセスすると、それっぽいデザインになっています。`required` 属性が付いた `input` 要素のフィールドが未入力だと赤枠で囲まれるのが素敵ですね。

<img width="399" alt="スクリーンショット 2019-12-02 3.22.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/5f2fe753-a2c4-3b7c-cc25-904da35ad0a4.png">

まあいろいろと、もう少し何とかしたい感はありますが、一切の class 付けもカスタマイズも行わずにただ CSS を読み込むだけで、ここまでスタイリングしてもらえれば充分です。今回はスタイリングのことは考えずに進めたいので、一旦はこのまま進もうと思います。

## 認証

次は、サインアップしたユーザーのアカウントIDとパスワードでログインできるようにします。この章では以下のような仕様で実装していきます。

- ログインしたらトップページへ遷移
- ログインしていない状態でトップページへアクセスされたらログインページへリダイレクト

### トップページ

まず、ログインした時に表示するためのトップページを作ります。トップページはタスクの一覧をいきなり表示するので、`TasksController` の `index` アクションにルーティングします。

```diff:config/routes.rb
  Rails.application.routes.draw do
+   root 'tasks#index'

    get 'signup', to: 'users#new'
    post 'signup', to: 'users#create'
  end
```

`app/controllers/tasks_controller.rb` を作成し、 `TasksController` の `index` アクションを仮実装します。今は、ログイン後にページが表示されれば何でも良いので空っぽのアクションで ok です。

```rb:app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  def index; end
end
```

続いて `app/views/tasks/index.html.erb` を作成し、View を仮実装します。テンションの上がりそうなテキストを置いておきましょう。

```erb:app/views/tasks/index.html.erb
<p>Welcome!!</p>
```

これで、トップページ([localhost:3000](http://localhost:3000))にアクセスするとテンションが上がります。
<img width="262" alt="スクリーンショット 2019-12-02 11.16.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/128d99eb-a776-b3cd-a355-0e826cf55afc.png">

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

サインアップページやトップページを作った時と同様に、サクサクとルーティングと Controller と View を実装していきましょう。少しずつ慣れてきましたか？

```diff:config/routes.rb
  Rails.application.routes.draw do
    root 'tasks#index'

    get 'signup', to: 'users#new'
    post 'signup', to: 'users#create'
+
+   get 'login', to: 'sessions#new'
+   post 'login', to: 'sessions#create'
  end
```

```rb:app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def new; end

  def create; end
end
```

```erb:app/views/sessions/new.html.erb
<%= form_with scope: :session, url: login_path, local: true do |f| %>
  <p>
    <%= f.label :account_id, 'アカウントID' %>
    <%= f.text_field :account_id, required: true %>
  </p>

  <p>
    <%= f.label :password, 'パスワード' %>
    <%= f.password_field :password, required: true %>
  </p>

  <p><%= f.submit 'ログイン' %></p>
<% end %>

<p><%= link_to 'サインアップはこちら', signup_path %></p>
```

サクサクと実装できましたか？これでログインページ([localhost:3000/login](http://localhost:3000/login))にアクセスするとフォームが表示されます。
<img width="354" alt="スクリーンショット 2019-12-03 6.11.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/f2a6301c-d615-1eea-5555-16080253d0b7.png">

あとは、サインアップ完了画面にログインリンクを追加しておきましょう。

```diff:app/views/users/create.html.erb
  <p>ご登録ありがとうございます。</p>
+ <p><%= link_to 'ログインページ', login_path %>からログインしてご利用を開始してください。</p>
```

### 認証

さて、それではログイン処理を実装していきましょう。いきなり難易度が跳ね上がりますが、あれだったらコピペして後は見なかったことにして切り抜けてください。まずは認証のコア部分を書いていきます。今回は超シンプルな認証にするので `devise` 等の Gem は使わずに remember me も実装せず、大したセキュリティも意識せずにサクッとお手製で作っていきます。認証系を手書きするのは地獄だと思われるかもしれませんが、これだけです。

```rb:app/helpers/sessions_helper.rb
module SessionsHelper
  def login(user)
    session[:user_id] = user.id
  end

  def current_user
    @current_user ||= User.find_by(id: session[:user_id])
  end

  def logged_in?
    current_user.present?
  end
end
```

`login` メソッドはただの代入のように見えますが、これは `User` の ID をクライアントの Cookie に暗号化して詰めています。この Cookie の中身を復号した値は代入時と同じように `session` メソッドから呼び出せます。つまり、`session[:user_id]` に `User` の ID を詰めておけば、次回以降のリクエストでユーザーに Cookie が残っていれば `session[:user_id]` を参照することで読み出すことができます。

`current_user` メソッドは、上述の `session[:user_id]` を使って `User` のインスタンスを取得して返します。`User.find_by(id: session[:user_id])` で Cookie に入っているユーザーの ID でデータベースを照会している訳ですね。`||=` という演算子の使い方は見慣れない方もいるでしょう。これは、演算子の短絡評価の特性を使って左辺が偽の場合のみ代入が実行するための文法です。論理和演算(`||`)における短絡評価とは、「左辺が偽なら右辺が真であろうが偽であろうがどうせ偽になるので右辺をそもそも評価する必要ないよね」という評価法です。左辺が真であって初めて右辺を気にするということですね。ここで `||=` を使う目的は、同じリクエスト内で複数回 `current_user` メソッドがコールされた時のデータベースアクセスを初回以外スキップすることです。つまり初回でデータベースから `User` のインスタンスを引っ張ってきたら、次回以降はインスタンス変数を参照することでデータベースにアクセスするコストを防いでいるということです。

`logged_in?` メソッドは、見ての通り `current_user` で `User` のインスタンスを引っ張れたかどうかを確認するメソッドです。`find_by` は見つからなかった時に `nil` を返すので、`session[:user_id]` が空の場合や存在しないユーザーの ID 等が入っている場合は、`logged_in?` メソッドが `false` を返すということです。

さて、ログイン機構を作ったところで、ログインフォームから POST されてきた時の処理を実装しましょう。`SessionsController` の `create` アクションを実装していきます。

```rb:app/controllers/sessions_controller.rb
  def create
    user = User.find_by(account_id: params[:session][:account_id])
    if user&.authenticate(params[:session][:password])
      login user
      redirect_to root_path
    else
      flash.now[:alert] = 'アカウントID、またはパスワードが間違っています。'
      render :new
    end
  end
```

ユーザーから POST された値(ログインフォームに入力されたアカウントIDとパスワード)は、`params[:session]` に入っているので、まず `User.find_by(account_id: params[:session][:account_id])` でそのアカウントIDのユーザーを引っ張り、次行の `if user&.authenticate(params[:session][:password])` でパスワードが正しいかを確認しています。`authenticate` メソッドは `User` モデルでコールした `has_secure_password` メソッドによって提供されていて、引数に与えたパスワードが正しいかどうかを返してくれます。`&.` という演算子も見慣れない方がいるかもしれません。これは Safe Navigation Operator という演算子で、レシーバーが nil であれば `NoMethodError` を投げずに `nil` を返してくれます。昔の Ruby を知っている方は `user.try!(:authenticate, params[:session][:password])` と同じだと考えると良いかもしれません。つまり、ユーザーが存在しないアカウントIDを POST してきて `find_by` で `User` のインスタンスが引っ張れずに `nil` を返してきた時に、`else` に入るために利用しています。`if user.present? && user.authenticate(params[:session][:password])` という上述の演算子の短絡評価を使った書き方の方が明示的で好みな方もいるかもしれませんね。余談ですが、`&` はひとりぼっちで体育座りしている様子に見えるため `&.` は通称「ぼっちオペレーター」と呼ばれていたりします。

そんなこんなで認証できた場合は `if` が真となるので、先ほど `SessionsHelper` に定義した `login` メソッドをコールして Cookie に `User` の ID を詰めてトップページにリダイレクトしています。`else` に入った場合は、POST されたアカウントIDが存在していなかったか、アカウントIDは存在していたけどパスワードが間違っていたかのいずれかなので、エラーメッセージを `flash` に詰めて `new.html.erb` を再描画します。詰めたエラーメッセージを表示する処理は後ほど書きましょう。その前に、今のままだと `SessionsHelper` の `login` メソッドが `SessionsController` からは見えていない問題を解決します。`SessionsHelper` の `current_user` はあちこちで使うことになるので全ての Controller が継承している `ApplicationController` で Mix-in します。

```diff:app/controllers/application_controller.rb
  class ApplicationController < ActionController::Base
+   include SessionsHelper
  end
```

これで、`ApplicationController` を継承している全ての Controller で `SessionsHelper` の `current_user` や `logged_in?` を利用できるようになりました。Helper を Controller に Mix-in するのって気持ち悪いですが、現状スッキリとした方法が思い浮かばないのでこのままいきます。

そして、ログインページに、ログインに失敗した時のエラーメッセージを表示します。先ほど Controller の `flash.now[:alert]`  で詰めたエラーメッセージが表示されます。`if flash[:alert].present?` しても良いですが、まあ `flash[:alert]` の中身が `nil` ならどうせ何も出ないので雑に一行置いておきましょう。

```diff:app/views/sessions/new.html.erb
  <%= form_with scope: :session, url: login_path, local: true do |f| %>
+   <%= flash[:alert] %>

    <p>
      <%= f.label :account_id, 'アカウントID' %>
      <%= f.text_field :account_id, required: true %>
    </p>
    ...
  <% end %>
```

最後に、ログインに成功してトップページにリダイレクトされた後、本当にログインに成功しているのかを確かめるために「Welcome!!」 の文字列を「Welcome [ユーザーの名前(`User#name`)]!!」に変更しましょう。

```diff:app/views/tasks/index.html.erb
- <p>Welcome!!</p>
+ <p>Welcome <%= current_user.name %>!!</p> 
```

これでログイン処理周りの実装は完了です。早速動かしてみましょう！まずは、適当なアカウントIDやパスワードを入力してログインに失敗した場合にエラーメッセージが表示されるかを確認しましょう。
<img width="376" alt="スクリーンショット 2019-12-03 6.12.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/14cc9923-08c6-e8f9-9480-8d1c586cd8be.png">


次に、正しいアカウントIDとパスワードでログインしてみましょう。先ほど作ったテストアカウント(アカウントID: `test`, パスワード: `password`) でログインしてみましょう。いろいろあってテストアカウントがない場合はサインアップページ([localhost:3000/signup](http://localhost:3000/signup))から作りましょう。ログインに成功してユーザー名が表示されれば ok です。
<img width="265" alt="スクリーンショット 2019-12-03 6.06.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/4d87d9ca-2c0a-6f76-0353-de545f6904f9.png">

### ログアウト

ログインできるようになったのでログアウトも実装しましょう。サクサクといきましょう。まずはルーティングを追加します。`/login` に対する DELETE リクエストを `SessionsController` の `destroy` アクションにルーティングします。

```diff:config/routes.rb
    get 'login', to: 'sessions#new'
    post 'login', to: 'sessions#create'
+   delete 'logout', to: 'sessions#destroy'
```

`SessionsController` の `destroy` を実装していきます。この後追加する `SessionsHelper` の `logout` メソッドをコールしてログインページにリダイレクトします。

```diff:app/controllers/sessions_controller.rb
    def create
      ...
    end
+
+   def destroy
+     logout
+     redirect_to login_path
+   end
  end
```

そして、実際のログアウト処理です。Cookie から `User` の ID 情報を削除して、インスタンス変数も `nil` で初期化するだけです。

```diff:app/helpers/sessions_helper.rb
    def logged_in?
      current_user.present?
    end
+
+   def logout
+     session.delete(:user_id)
+     @current_user = nil
+   end
  end
```

最後にログアウトのリンクを置きます。まあとりあえずトップページにでも置いておきましょう。

```diff:app/views/tasks/index.html.erb
  <p>Welcome <%= current_user.name %>!!</p>
+
+ <%= link_to 'ログアウト', logout_path, method: :delete %>
```

最後と言ったな、あれは嘘だ。まだ大きな問題があります。上記のコードでは `link_to` に `method` オプションを与えることで、生成される `a` 要素に `data-method="delete"` 属性を追加して GET ではなく DELETE でリクエストを送ることを期待しています。しかし、現状はそんなことはできずに GET リクエストで `/logout` にリクエストされてルーティングエラーになります。「えっ？よくやってるけど」と思う方もいらっしゃるかもしれませんが、これは [jquery-ujs](https://github.com/rails/jquery-ujs) が提供している機能です。今回は `--skip-javascript` で `rails new` したため jquery-ujs なんて高級なものは入っていません。よし、CDN から読み込みましょう。(雑)

```diff:app/views/layouts/application.html.erb
      <link rel="stylesheet" href="https://cdn.rawgit.com/Chalarangelo/mini.css/v3.0.1/dist/mini-default.min.css">
      <%= stylesheet_link_tag    'application', media: 'all' %>
+
+     <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
+     <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-ujs/1.2.2/rails.js"></script>
    </head>
```

ちなみに `data-method` 属性を持った `a` 要素で任意のリクエストを投げるためのコードは[これ](https://github.com/rails/jquery-ujs/blob/master/src/rails.js#L215)です。なんと、`a` 要素が押された瞬間に `data-method` に従ったメソッドでリクエストを投げられるような `form` 要素を錬金して強制 `submit` しています。最初から `form` を組み立てておいても良いなら `link_to` ではなく `button_to` を使う方法もあるのですがデザインがいい感じにならなかったので jquery-ujs を読み込んじゃうことにしました、てへぺろ。まあ今回はしばらく使い古した web アプリケーションのモバイルアプリ化が目的なので、ちょっと jQuery ぐらい置いてたり、設計が歪だったりした方がリアルじゃないですか。ちなみに `rails-ujs` の話がタイムリーなことに別の Advent Calendar の記事で行われていたので、詳しく知りたい方は[こちら](https://www.inodev.jp/entry/2019/12/03/234210)の記事をどうぞ。

さて、ということで、ログアウト処理が実装できました。ログインしてトップページからログアウトリンクを踏むとログインページに戻されるためです。ブラウザーのデベロッパーツールでログアウト前とログアウト後で Cookie がどのような動きをするかを確認してみても良いかもしれませんね。

<img width="264" alt="スクリーンショット 2019-12-03 23.27.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/5b1805ca-d819-dde2-4bc3-6bb66bb05eba.png">

### ログイン必須化

ログインとログアウトの処理は実装しましたが、もう1つだけ問題があります。それは、ログアウトした状態であってもトップページにアクセスできてしまうことです。ログインしていない状態で [localhost:3000](http://localhost:3000) にアクセスしてみましょう。このように「`current_user` が `nil` だから `name` なんかにはアクセスできませんぞ！！」というエラーが表示されます。

<img width="627" alt="スクリーンショット 2019-12-03 23.29.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/5847f1f7-797c-50e2-2b61-29d96bf352be.png">

ログアウト状態では、`session[:user_id]` が空なので `current_user` は `nil` を返すのでしたね、覚えていますか？回避方法としては先ほどの Safe Navigation Operator (通称ぼっちオペレーター)を使って `current_user&.name` にする手もありますが、そもそも今回の要件としては、トップページはログインユーザー以外アクセスできてはならないということなので、未ログインのユーザーがトップページにアクセスしてきたらログインページにリダイレクトするようにしましょう。

```diff:app/controllers/tasks_controller.rb
  class TasksController < ApplicationController
+   before_action :login_required

    def index; end
+
+   private
+
+   def login_required
+     redirect_to login_path unless logged_in?
+   end
  end
```

`before_action` にシンボルで渡したメソッドは、この Controller のアクションがコールされる前に実行されます。ここでは、`unless logged_in?` の時、つまり未ログインの時にログインページにリダイレクトしています。では、ログアウトした状態でトップページにアクセスしてみましょう。ログインページにリダイレクトされており、ログインしたらトップページが表示されれば ok です。あとは肝心のタスク管理ができるようになれば web アプリケーションとしては完成です。

## タスクの CRUD

CRUD とは、データの Create, Read, Update, Delete の頭文字を取ったものです。つまり、タスクを作ったり読み出したり更新したり消したりといったコードをこれから書いていきます。今回は Delete は実装しないので、CRU かもしれませんね。

### 一覧ページと作成フォーム

タスクの一覧ページはトップページとユーザーIDを指定したページの2種類用意します。つまり、[localhost:3000](http://localhost:3000) と [localhost:3000/users/:id/tasks](http://localhost:3000/users/:id/tasks) の2種類です。`:id` の部分にはその `User` の `id` 属性が入ります。また、タスクの一覧ページに正しくタスクが表示されているかを確認するために、一気にタスク作成フォームまで作ってしまうので `create` アクションも実装します。今回は `index` ページにタスク追加フォームを配置するので `new` の専用フォームはありません。`index` と `create` を同時に実装していくのは難しそうに感じるかもしれませんが、大丈夫、あなたは写経するだけです。

```diff:config/routes.rb
    get 'login', to: 'sessions#new'
    post 'login', to: 'sessions#create'
    delete 'logout', to: 'sessions#destroy'
+
+   resources :users, only: [] do
+     resources :tasks, only: :index
+   end
+   resources :tasks, only: :create
```

それでは、タスクの一覧を表示するための Controller 処理を書いていきます。今回少し癖があるのは、`/users/:id/tasks` の URL でアクセスされた場合は `:id` で指定された `User` のタスクを取得しますが、そうでない場合はログイン中の `User(current_user)` のタスクを取得する必要があるということです。

```diff:app/controllers/tasks_controller.rb
  class TasksController < ApplicationController
    before_action :login_required
+   before_action :set_user

-   def index; end
+   def index
+     @tasks = @user.tasks.where(created_at: Time.zone.today.all_day)
+   end
+
+   def create
+     @task = Task.create(task_params.merge(user: current_user))
+     redirect_to Rails.application.routes.recognize_path(request.referer)
+   end

    private

    def login_required
      redirect_to login_path unless logged_in?
    end
+
+   def set_user
+     @user = (params[:user_id].present? ? User.find(params[:user_id]) : current_user)
+   end
+
+   def task_params
+     params.require(:task).permit(:name, :finished)
+   end
  end
```

まずは `before_action` で今回タスクを表示する `User` のインスタンスを `@user` に代入しています。もし、`params[:user_id]` が存在しているのであれば、`/users/:id/tasks` からのリクエストなので `User.find(params[:user_id])` で対象の `User` のインスタンスを取得します。`params[:id]` が存在していなければトップページからのリクエストなので `current_user` を取得します。`index` アクションでは、この `@user` が持っている今日付けのタスクを全て取得しています。`create` アクションでは、飛んできたパラメーターにログインユーザーの `user_id` をセットして `tasks` テーブルのレコードを作成し、元いたページ(トップページ or `/users/:id/tasks`)にリダイレクトしています。

次に View を書いていきましょう。Welcome メッセージとログアウトリンクだけであったページを、タスク一覧とタスク作成フォームの表示ページとしてマークアップしていきます。

```erb:app/views/tasks/index.html.erb
<p><%= @user.name %></p>

<% if @user == current_user %>
  <%= form_with model: Task.new, local: true do |f| %>
    <%= f.text_field :name, required: true %>
    <%= f.submit '作成' %>
  <% end %>
<% end %>

<ul>
  <% @tasks.each do |task| %>
    <li>
      <%= check_box_tag "finished#{task.id}", true, task.finished, data: { id: task.id }, disabled: (@user != current_user) %>
      <%= label_tag "finished#{task.id}", task.name %>
    </li>
  <% end %>
</ul>

<%= link_to 'ログアウト', logout_path, method: :delete %>

<% if @user == current_user %>
  <script>
    $(document).ready(function(){
      $('#task_name').focus();
    });
  </script>
<% end %>

<style>
  li { list-style: none; }

  :checked + label {
    color: gray;
    text-decoration: line-through;
  }
</style>
```

今回のアプリケーションでは、タスクのチェックボックスにチェックを入れた状態を完了状態とします。ただし、人のタスクを勝手に完了にするわけにはいかないので、`disabled: (@user != current_user)` で今見ているページが自分のページでない時に限りチェックボックスを非活性にしています。チェックボックスをチェックした時にサーバーにリクエストを送って `finished` を変更する処理は後ほど書いていきます。今はただチェックボックスを表示するだけのコードです。`check_box_tag` の各種引数は覚えるかググるかしかないので、まあ適当に察してください。フォームヘルパーの引数順は何百回ググっても覚えられませんね。まあ、都度ググれば良いのです。チェックボックスの活性状態と同様に、タスクの新規作成フォームも `if @user == current_user` で今見ているページが自分のページである場合のみ表示しています。

今回の View には純粋な HTML の文書構造だけではなく、`script` 要素と `style` 要素を使っています。`script` 要素では、JavaScript でページを表示した時にタスク名の入力フォームにフォーカスが当たるようにしています。こうすることで、タスク名を打って return キーを叩く操作を繰り返すだけで次々にタスクを作っていくことができて快適になります。ここでも jQuery を利用しています。`style` 要素では軽くデザインを当てていて、`li` 要素の `list-style: none;` で箇条書きの `・` を非表示にして、チェックされたチェックボックスの後続の `label` 要素のテキストカラーをグレーにして取り消し線を付けています。まあどちらも触ってみれば効果が分かるでしょう。

ということで動作確認です。少し複雑な仕様で作り込んだのでテストも複雑です。テストコードを書いた方が早くテストできるのでストレスは溜まりますが、一生懸命ブラウザーで手動確認していきましょう。準備として、別のユーザーをサインアップフォームから作成しておいてください。サインアップを何度か繰り返していて、もう今のデータベースの状態がよく分からなくなってしまった方は、一度データベースをリセットしてゼロから作っていきましょう。

```zsh
% rails db:migrate:reset
% rails c
irb(main):001:0> User.create(name: 'テストアカウント', account_id: 'test', password: 'password', password_confirmation: 'password')
irb(main):002:0> User.create(name: 'テストアカウント2', account_id: 'test2', password: 'password', password_confirmation: 'password')
irb(main):003:0> exit
```

2ユーザーをシステムに作成できたらログインとログアウトを繰り返しながら双方のタスクをいくつか作ってみて、以下を確認してみましょう。

- トップページ([localhost:3000](http:localhost:3000))
  - 自分のタスクが表示されること
  - タスクの作成が行えること
  - チェックボックスをチェックできること

<img width="303" alt="スクリーンショット 2019-12-05 7.43.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/1e71853d-1afe-2dad-27e0-8bb4d2424fde.png">

- 自分のタスク一覧ページ([localhost:3000/users/1/tasks](http:localhost:3000/users/1/tasks))
  - 自分のタスクが表示されること
  - タスクの作成が行えること
  - チェックボックスをチェックできること

<img width="342" alt="スクリーンショット 2019-12-05 7.41.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/0fc3f4e1-1081-2b5b-cc69-0aa04176f9fe.png">

- 他人のタスク一覧ページ([localhost:3000/users/2/tasks](http:localhost:3000/users/2/tasks))
  - 他人のタスクが表示されること
  - タスク作成フォームが表示されていないこと
  - チェックボックスをチェックできないこと

<img width="338" alt="スクリーンショット 2019-12-05 7.44.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/a895c2bc-f765-be0b-9a6e-f553d150b6eb.png">

### タスクの更新

チェックボックスをチェックすることで、`tasks` テーブルの `finished` 列を更新します。今回はチェックボックスの状態変更のイベントを拾って Ajax で非同期にサーバーにステータス更新リクエストを投げることにします。まずはルーティングに `update` アクションを追加します。

```diff:config/routes.rb
    resources :users, only: [] do
      resources :tasks, only: :index
    end
-   resources :tasks, only: :create
+   resources :tasks, only: %i[create update]
```

次に `TasksController` の `create` アクションの下に `update` アクションを追加しましょう。上述のルーティングによって `/tasks/:id` という URL でリクエストが送られてくるので、`params[:id]` の `Task` を更新します。ただし、更新しようとしているタスクがログインしているユーザーのタスクであった場合のみ更新を行い、ログインしているユーザーのタスクでなかった場合はログインページにリダイレクトしています。勝手に他人のタスクを弄られないようにする配慮です。

```rb:app/controllers/tasks_controller
  def update
    task = Task.find(params[:id])
    redirect_to login_path if task.user != current_user
    task.update(task_params)
  end
```

そして、View の `script` 要素内にチェックボックスの状態が変化した時に上記の Controller までリクエストを投げる処理を追加します。

```diff:app/views/tasks/index.html.erb
    <script>
      $(document).ready(function(){
        $('#task_name').focus();
      });
+
+     $('input[type="checkbox"]').change(function() {
+       $.ajax('/tasks/' + $(this).data('id'),
+         {
+           type: 'patch',
+           data: {
+             task: {
+               finished: $(this).prop('checked')
+             }
+           },
+           dataType: 'json'
+         }
+       )
+     });
    </script>
```

チェックボックスを触る度にページをリロードされてはストレスなので、Ajax で非同期にリクエストを投げることでページ遷移を挟まずにリクエストを投げています。やや複雑に見えるコードですが、落ち着いて1行ずつ読んでみれば意味は分かるはずです。`$('input[type="checkbox"]').change` はチェックボックスが変更された場合イベントを拾おうとしています。`$.ajax('/tasks/' + $(this).data('id')` では Ajax でリクエストを送る URL を組み立てています。変更されたチェックボックスである `$(this)` の `data-id` 属性を使って `/tasks/1` といった URL を作っています。`data-id` 属性についてピンと来なければ `check_box_tag` にどんなパラメーターを与えたかと、結果としてどのような HTML が生成されているかをブラウザーからソースコードを見て確認してみましょう。`type: 'patch'` は PATCH リクエストを送るためです。PATCH がピンと来なければ `rails routes` コマンドを実行して `config/routes.rb` によってどのような URL とメソッドがアプリケーションに定義されているかを確認してみましょう。`data: { task: { finished: $(this).prop('checked') } }` はサーバーに送るパラメーターです。動作確認する時にチェックボックスを操作した時に `rails s` に出るログを確認してチェックを付けた時と外した時でそれぞれどんなパラメーターが飛んできているかを確認してみましょう。ということで、早速動かしてみましょう。チェック状態がページをリロードしても変わらなければ ok です。

![reload.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/b96bf0d5-8267-53d6-0432-77df02317775.gif)

なんと、これで web アプリケーションとしては完成です。日次のタスクをひたすら登録して消化していくには充分な機能ができました。勿論、まだまだ改善の余地はあるのでスタイリングをするなり、ユーザーのお気に入り機能を付けるなり、他人のタスクを見るだけならログイン必須ではなくしたり、いろいろ取り組んでみると力が付くかもしれませんね。とりあえず、本記事で作りたかった web アプリケーションはこれで完成です。お疲れ様でした。

# モバイルアプリケーション用 API 実装

ここからは、モバイルアプリケーションからこの web アプリケーションを扱えるように API を実装していきます。外部からでもタスクの情報を取得したり、タスクの完了状態を変更したりできるようにするためのインターフェースを提供するということですね。早速やっていきましょう。

## 認証

今作った web アプリケーションでは、タスクを見るにも作るにも更新するにもアカウントIDとパスワードによるログイン認証が必要でした。モバイルアプリケーションも同様で、正しいアカウントIDとパスワードを入力したモバイルアプリケーションにのみタスクの操作を許してあげる必要があります。web アプリケーションは、モバイルアプリケーションが正しいアカウントIDとパスワードを送ってきた場合は API トークンを返します。web アプリケーションは、モバイルアプリケーションがパラメーターに乗せてきた API トークンでログインユーザーを判別します。何を言っているのか分からないと思いますので、実装を始めてしまいましょう。作っていくうちに何となく分かってくるはずです。

### APIトークンの発行

まずは `users` テーブルに `api_token` の列を追加します。

```zsh
% rails g migration AddApiTokenToUsers api_token:string
% rails db:migrate
```

そして、`users` テーブルにレコードを追加する時に自動でAPIトークンを埋めるようにします。何と1行追加するだけで `bcrypt` が勝手にやってくれます。

```diff:app/models/user.rb
  class User < ApplicationRecord
    has_many :tasks

    has_secure_password
+   has_secure_token :api_token

    validates :name, presence: true
    validates :account_id, presence: true, uniqueness: true
  end
```

これで新規の `User` に関しては自動で `api_token` がセットされるようになりましたが、既に存在する `users` テーブルのレコードの `api_token` は空のままなので `rails console` からワンライナーで一気に発行してしまいましょう。ちゃんと発行されたか不安であれば `User.first.api_token` 等で確認してみましょう。これで API トークンの発行準備は完了です。

```zsh
% rails c
irb(main):001:0> User.find_each { |user| user.regenerate_api_token }
```

### ログイン

次に、ログイン用のルーティングを追加します。API に関する URL は必ず `/api` で始めることにしましょう。また、API 用の Controller は `Api` という名前空間で括ることにしましょう。つまり、ログイン API は URL が `/api/login` で Controller は `app/controllers/api/login_controller.rb` にある `Api::LoginController` クラスとなります。

```diff:config/routes.rb
    resources :tasks, only: %i[create update]
+
+   namespace :api do
+     get 'login', to: 'login#show'
+   end
```

次に Controller を実装していきます。

```rb:app/controllers/api/login_controller.rb
class Api::LoginController < ApplicationController
  def show
    @user = User.find_by(account_id: params[:account_id])
    if @user&.authenticate(params[:password])
      render status: :ok, json: { api_token: @user.api_token }
    else
      render status: :unauthorized, json: {}
    end
  end
end
```

それでは、[localhost:3000/api/login?account_id=test&password=password](http://localhost:3000/api/login?account_id=test&password=password) にアクセスしてみましょう。このような API トークンが表示されれば ok です。

<img width="549" alt="スクリーンショット 2019-12-06 3.00.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/096d7cc9-9a73-d96a-39e7-e7464dd11e7b.png">

また、パラメーターに誤ったアカウントIDかパスワードを指定してみましょう。この時は API トークンは返ってきません。

<img width="532" alt="スクリーンショット 2019-12-06 2.59.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/5de4ded4-d7ab-5fcc-f578-52753f6c9771.png">

## タスク CRUD API

### 認証と一覧の取得

自身のタスクの一覧を返したり、更新させたりという機能を書いていくわけですが、それを要求してきたユーザーが上記の API トークンをちゃんと持っているかを確認する必要があります。ルールとして、リクエストのパラメーターに必ず `api_token` を与えてもらうことにします。`api_token` のパラメーターが渡されていない場合、あるいは誤っている場合のリクエストは無視します。 

```app/controllers/api/tasks_controller.rb
class Api::TasksController < ApplicationController
  before_action :authenticate_by_token

  def index
    @tasks = @user.tasks.where(created_at: Time.zone.today.all_day)
    render json: @tasks.map { |task| { id: task.id, name: task.name, finished: task.finished } } 
  end

  private

  def authenticate_by_token
    @user = User.find_by(api_token: params[:api_token])
    render status: :unauthorized, json: 'Invalid API token' if @user.blank?
  end
end
```

`before_action` では、トークンの認証を行い失敗した場合に `Invalid API token` という文字列とステータスコード 401 を返します。

<img width="419" alt="スクリーンショット 2019-12-06 3.56.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/3c4c5937-3d70-c395-328f-dd01669b51e8.png">

正しい API トークンを与えた場合はタスクの一覧の json が返ってきます。これが `@tasks.map { |task| { name: task.name, finished: task.finished } }` の結果です。モバイルアプリケーションは、この情報を元にタスクの一覧画面を組み立てるわけですね。

<img width="675" alt="スクリーンショット 2019-12-06 3.57.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/353d0045-b0e3-23b9-1343-443cb91b87d9.png">

### 作成と更新

続いて作成と更新の API を一気に書いていきます。やることは web アプリケーションとほとんど同じなのでサクサクといきましょう。まずはルーティングに `create` と `update` アクションを追加します。

```diff:config/routes.rb
    namespace :api do
      get 'login', to: 'login#show'
-     resources :tasks, only: :index
+     resources :tasks, only: %i[index create update]
    end
```

そして、Controller に web の時と同様に `create` と `update` アクションを実装していきます。

```diff:app/controllers/api/tasks_controller.rb
  class Api::TasksController < ApplicationController
+   skip_forgery_protection

    before_action :authenticate_by_token

    def index
      @tasks = @user.tasks.where(created_at: Time.zone.today.all_day)
      render json: @tasks.map { |task| { id: task.id, name: task.name, finished: task.finished } } 
    end
+
+   def create
+     task = Task.create(task_params.merge(user: @user))
+     render json: { id: task.id, name: task.name, finished: task.finished }, status: :created
+   end
+
+   def update
+     task = Task.find(params[:id])
+     (task.user == @user) ? task.update(task_params) : render(status: :unauthorized)
+   end

    private

    def authenticate_by_token
      @user = User.find_by(api_token: params[:api_token])
      render status: :unauthorized, json: 'Invalid API token' if @user.blank?
    end
+
+   def task_params
+     params.require(:task).permit(:name, :finished)
+   end
  end
```

`create` アクションと `update` アクションの中身はほとんど前回と同じなので問題ないでしょう。問題があるとすれば先頭にしれっと追加された `skip_forgery_protection ` という一行です。これは話すと長くなるのですが、Ruby on Rails はデフォルトで CSRF(Cross-Site Request Forgeries) 保護が行われています。外部の攻撃者から不正なリクエストを受けても弾けるように、POST や PATCH リクエストは認証トークンを添えて送らなければリクエストを処理しません。認証トークンで認証できない場合は Controller の `before_action` 時点で弾かれるので `create` や `update` といったアクションに入ってくることはありません。`skip_forgery_protection` はこれを `skip_before_action` するためのものです。この後 `curl` で API を叩いてみるので興味のある方は `skip_forgery_protection` をコメントアウトした状態で API を叩いて `rails server` のログを確認してみると良いでしょう。`Can't verify CSRF token authenticity.` というエラーが表示されているはずです。そして作成や更新は行われずにデータベースに変化は起こっていないはずです。ただ、この Controller に関しては、`authenticate_by_token` メソッドで自前の ~~セキュリティゆるゆるの~~ トークン認証を実装しているので、一旦は CSRF 保護をスキップします。

それでは、動作を確認してみましょう。まずは作成です。ターミナルから `curl` コマンドを実行してサーバーに POST リクエストを送ってみます。`api_token` はご自身のものに差し替えてください。先ほどログイン API で GET した API トークンです。

```zsh
% curl -X POST -d 'task[name]=by API&api_token=8YKPhsDNphD91j4EaQxyf6JF' localhost:3000/api/tasks
```

実行したらブラウザーのタスク一覧画面か、一覧 API で返ってくる json を確認してみましょう。新しいタスクが追加されていれば ok です。

<img width="306" alt="スクリーンショット 2019-12-06 4.29.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/ada244c2-3d1f-ea11-83d1-dfea301adeb7.png">

POST による作成が問題なければ、次は更新の PATCH リクエストを送ってみましょう。今作ったタスクを完了状態にしてみましょう。API トークンを差し替えるのは勿論のこと、URL の末尾の `Task` の ID も今作った `Task` の ID に差し替えてください。`rails console` で `Task.last.id` を叩けば最後に作った `Task` の ID が取れます。

```zsh
% curl -X PATCH -d 'task[finished]=true&api_token=8YKPhsDNphD91j4EaQxyf6JF' localhost:3000/api/tasks/7
```

再びブラウザーのタスク一覧画面か、一覧 API で返ってくる json を確認してみましょう。完了状態になっている、あるいは `finished` が `true` になっていれば ok です。

<img width="306" alt="スクリーンショット 2019-12-06 4.31.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/d9425b92-e94a-3acc-8b55-96c2614d1796.png">

ここまで実装できれば、モバイルアプリケーションからタスク管理するための準備は整いました。お疲れ様でした。

# デプロイ

最後にこの web アプリケーションをどこかのサーバーに置く必要があります。本記事では無料かつクレジットカードの登録も必要がない [Heroku](https://jp.heroku.com/) にデプロイする手順を雑に書きますが、お好きなサーバーに上げてもらって大丈夫です。どこにも上げたくなくてお手軽にやりたければ、[ngrok](https://ngrok.com) で `localhost:3000` をポートフォワーディングする手もあります。本記事では「web アプリケーションを作ったった！！」という気分になりたいので、とりあえず Heroku の無料枠にデプロイします。

まずは [Heroku](https://jp.heroku.com/) にサインアップして[インストールガイド](https://devcenter.heroku.com/articles/heroku-cli)に従って heroku-cli をインストールします。Mac であれば現状は以下です。

```zsh
% brew tap heroku/brew && brew install heroku
```

Heroku にログインします。下記のコマンドを実行するとブラウザーからのログインが求められます。

```zsh
% heroku login
```

Heroku では Git のリポジトリを push する形でデプロイするので、もしここまで Git 管理していなかった人は雑に commit してください。

```zsh
% git commit -am "Create task management application"
```

Heroku にアプリを作ります。このタイミングで URL が割り振られます。

```zsh
% heroku create
Creating app... done, ⬢ limitless-earth-31665
https://limitless-earth-31665.herokuapp.com/ | https://git.heroku.com/limitless-earth-31665.git
```

お好みで名前を変更してください。ドメインになります。勿論人と被ると弾かれます。`zone-web.herokuapp.com` は頂いた。

```zsh
% heroku rename zone-web
Renaming limitless-earth-31665 to zone-web... done
https://zone-web.herokuapp.com/ | https://git.heroku.com/zone-web.git
```

`heroku create` した段階で、`git remote` に `heroku` が自動登録されているので、ここに push するだけでデプロイできます。3分ぐらい時間がかかると思うので、お茶でも飲みながら休憩しましょう。

```zsh
% git push heroku master
```

無事にデプロイできたら、Heroku 上で `rails db:migrate` を実行してもらいます。`heroku run` を頭に付けるだけでコマンドを走らせられるのは素晴らしいですね。

```zsh
% heroku run rails db:migrate
```

これで準備は完了です。`heroku create` か `heroku rename` の時に割り振られた URL へアクセスしてみましょう。ログインページにリダイレクトされれば、ちゃんとアプリケーションが動いています。サインアップして自由にアプリケーションを操作してみましょう。

<img width="352" alt="スクリーンショット 2019-12-06 7.23.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/fa028e55-2cb8-ec6f-2ab2-167147995131.png">

これで、web アプリ編の演習は終了です。お疲れ様でした。[モバイルアプリ編](https://qiita.com/OgiharaRyo/private/9d16c935bcf769c82bc4) でお会いしましょう。


# 終わりに

今回の演習では、最小限の認証付きタスク管理アプリケーションを開発し、モバイルアプリケーションに向けた API の実装を行いました。もし完走した方がいらっしゃいましたら、こっそり私([@OgiharaRyo](https://twitter.com/OgiharaRyo))まで教えて頂けると嬉しいです。冒頭でも言い訳しましたが、Advent Calendar でやるには本当に本当に長い演習になってしまいました。重ねてお詫び申し上げます。

Ruby on Rails Advent Calendar 17日目は [bake0937](https://qiita.com/bake0937) さんの [「GoRails」は Ruby on Rails と英語を勉強するのにピッタリなサービスだった](https://bake0937.hatenablog.com/entry/2019/12/17/143125) です。