Help us understand the problem. What is going on with this article?

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

イントロダクション

目的

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

今回作るアプリケーションの完成品は以下のリポジトリに置いていますので、React Native 側の演習のみを行いたい方は Ruby on Rails アプリケーションは以下より clone してデプロイし、モバイルアプリ編 へ進んでください。

フレームワーク リポジトリ
Ruby on Rails (web) https://github.com/ogihara-ryo/zone-web
React Native (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 を叩けば誰でもそのユーザーの今日のタスク状況が見られるようにしましょう。

スクリーンショット 2019-12-13 11.25.13.pngスクリーンショット 2019-12-13 11.26.12.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 までご連絡頂くか、現在300人ほど参加している技術質問 slack コミュ二ティを運営していますので、こちらで質問して頂ければと思います。(slack 招待リンク)

環境

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

% 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

謝辞

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

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

rails new

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

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

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

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

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

スクリーンショット 2019-11-30 18.55.58.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 した結果はこのようになっているはずです。

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 を複数持つことはモデルが知らないので一文追加しておきましょう。

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

サインアップ

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

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

ルーティング

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

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 アクションはまだ書きません。

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

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

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

Gemfile
- # gem 'bcrypt', '~> 3.1.7'
+ gem 'bcrypt', '~> 3.1.7'

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

% bundle

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

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

View

新しく app/views/users/new.html.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 にアクセスするとフォームが表示されます。
スクリーンショット 2019-12-01 18.56.01.png

サインアップ処理実装

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

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 を作ってマークアップします。

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

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

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

スクリーンショット 2019-12-01 20.57.31.png

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

スクリーンショット 2019-12-01 21.09.20.png

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

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

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

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に関するバリデーションを定義していきます。

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 にして、パスワードとパスワード(確認)に別の文字列を入れてみます。

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 ブロックの一番上にエラーメッセージ表示用のマークアップを行いましょう。

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 にして、パスワードとパスワード(確認)に別の文字列を入れてみます。

スクリーンショット 2019-12-02 2.08.57.png

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

スクリーンショット 2019-12-02 2.10.23.png

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

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

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

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 です。

スクリーンショット 2019-12-02 2.57.29.png

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

スタイリング

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

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 要素のフィールドが未入力だと赤枠で囲まれるのが素敵ですね。

スクリーンショット 2019-12-02 3.22.01.png

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

認証

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

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

トップページ

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

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 を作成し、 TasksControllerindex アクションを仮実装します。今は、ログイン後にページが表示されれば何でも良いので空っぽのアクションで ok です。

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

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

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

これで、トップページ(localhost:3000)にアクセスするとテンションが上がります。
スクリーンショット 2019-12-02 11.16.20.png

ログインフォーム実装

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

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
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def new; end

  def create; end
end
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)にアクセスするとフォームが表示されます。
スクリーンショット 2019-12-03 6.11.50.png

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

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

認証

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

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

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

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_byUser のインスタンスが引っ張れずに nil を返してきた時に、else に入るために利用しています。if user.present? && user.authenticate(params[:session][:password]) という上述の演算子の短絡評価を使った書き方の方が明示的で好みな方もいるかもしれませんね。余談ですが、& はひとりぼっちで体育座りしている様子に見えるため &. は通称「ぼっちオペレーター」と呼ばれていたりします。

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

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

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

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

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)]!!」に変更しましょう。

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

これでログイン処理周りの実装は完了です。早速動かしてみましょう!まずは、適当なアカウントIDやパスワードを入力してログインに失敗した場合にエラーメッセージが表示されるかを確認しましょう。
スクリーンショット 2019-12-03 6.12.49.png

次に、正しいアカウントIDとパスワードでログインしてみましょう。先ほど作ったテストアカウント(アカウントID: test, パスワード: password) でログインしてみましょう。いろいろあってテストアカウントがない場合はサインアップページ(localhost:3000/signup)から作りましょう。ログインに成功してユーザー名が表示されれば ok です。
スクリーンショット 2019-12-03 6.06.20.png

ログアウト

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

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

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

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

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

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

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

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

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

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 要素で任意のリクエストを投げるためのコードはこれです。なんと、a 要素が押された瞬間に data-method に従ったメソッドでリクエストを投げられるような form 要素を錬金して強制 submit しています。最初から form を組み立てておいても良いなら link_to ではなく button_to を使う方法もあるのですがデザインがいい感じにならなかったので jquery-ujs を読み込んじゃうことにしました、てへぺろ。まあ今回はしばらく使い古した web アプリケーションのモバイルアプリ化が目的なので、ちょっと jQuery ぐらい置いてたり、設計が歪だったりした方がリアルじゃないですか。ちなみに rails-ujs の話がタイムリーなことに別の Advent Calendar の記事で行われていたので、詳しく知りたい方はこちらの記事をどうぞ。

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

スクリーンショット 2019-12-03 23.27.28.png

ログイン必須化

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

スクリーンショット 2019-12-03 23.29.16.png

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

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:3000localhost:3000/users/:id/tasks の2種類です。:id の部分にはその Userid 属性が入ります。また、タスクの一覧ページに正しくタスクが表示されているかを確認するために、一気にタスク作成フォームまで作ってしまうので create アクションも実装します。今回は index ページにタスク追加フォームを配置するので new の専用フォームはありません。indexcreate を同時に実装していくのは難しそうに感じるかもしれませんが、大丈夫、あなたは写経するだけです。

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) のタスクを取得する必要があるということです。

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 メッセージとログアウトリンクだけであったページを、タスク一覧とタスク作成フォームの表示ページとしてマークアップしていきます。

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 要素のテキストカラーをグレーにして取り消し線を付けています。まあどちらも触ってみれば効果が分かるでしょう。

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

% 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)
    • 自分のタスクが表示されること
    • タスクの作成が行えること
    • チェックボックスをチェックできること

スクリーンショット 2019-12-05 7.43.18.png

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

スクリーンショット 2019-12-05 7.41.48.png

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

スクリーンショット 2019-12-05 7.44.45.png

タスクの更新

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

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

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

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 までリクエストを投げる処理を追加します。

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

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

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

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

認証

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

APIトークンの発行

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

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

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

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 トークンの発行準備は完了です。

% 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 クラスとなります。

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

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

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 にアクセスしてみましょう。このような API トークンが表示されれば ok です。

スクリーンショット 2019-12-06 3.00.08.png

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

スクリーンショット 2019-12-06 2.59.36.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 を返します。

スクリーンショット 2019-12-06 3.56.08.png

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

スクリーンショット 2019-12-06 3.57.38.png

作成と更新

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

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 の時と同様に createupdate アクションを実装していきます。

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 時点で弾かれるので createupdate といったアクションに入ってくることはありません。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 トークンです。

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

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

スクリーンショット 2019-12-06 4.29.17.png

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

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

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

スクリーンショット 2019-12-06 4.31.23.png

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

デプロイ

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

まずは Heroku にサインアップしてインストールガイドに従って heroku-cli をインストールします。Mac であれば現状は以下です。

% brew tap heroku/brew && brew install heroku

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

% heroku login

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

% git commit -am "Create task management application"

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

% 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 は頂いた。

% 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 remoteheroku が自動登録されているので、ここに push するだけでデプロイできます。3分ぐらい時間がかかると思うので、お茶でも飲みながら休憩しましょう。

% git push heroku master

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

% heroku run rails db:migrate

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

スクリーンショット 2019-12-06 7.23.52.png

これで、web アプリ編の演習は終了です。お疲れ様でした。モバイルアプリ編 でお会いしましょう。

終わりに

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

Ruby on Rails Advent Calendar 17日目は bake0937 さんの 「GoRails」は Ruby on Rails と英語を勉強するのにピッタリなサービスだった です。

OgiharaRyo
ソフトウェアエンジニア / 個人事業主 / Ruby on Rails / dvorak配列 / Ergodox EZ / 在宅フルフレックス勤務絶対主義 / 労働は1日1時間目標 / 眼精疲労重症 / ヘビーゲーマー
https://ogihara-ryo.github.io
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした