この章でやること
- ユーザー登録機能を実装する
- HTMLフォームからアプリへ情報を送信すること
- その情報をデータベースへ登録する
- 登録されたユーザーのプロフィールを作成するページを作る
- ここから少し難易度が上がる
7.1 ユーザーを表示する
まずユーザーの名前とプロフィール写真を表示するためのページを作成する
バージョン管理を使っている場合は、いつもと同じようにトピックブランチを作成。
$ git checkout -b sign-up
7.1.1 デバッグとRails環境
サイトのレイアウトにデバッグ情報を追加する
これにより、ビルトインのdebugメソッドとparams変数を使って、各プロフィールページにデバッグ用の情報が表示されるようになる
<!DOCTYPE html>
<html>
.
.
.
<body>
<%= render 'layouts/header' %>
<div class="container">
<%= yield %>
<%= render 'layouts/footer' %>
<%= debug(params) if Rails.env.development? %>
</div>
</body>
</html>
if Rails.env.development?
こうしておくと、デバッグ情報は開発環境(development)だけで表示されるようになる
Railsの3つの環境
Railsには
- テスト環境(test)
- 開発環境(development)
- 本番環境(production)
の3つの環境がデフォルトで装備されている。
例えば、Rails Consoleのデフォルト環境はdevelopmentである。
$ rails c
irb(main):001:0> Rails.env
=> "development"
irb(main):002:0> Rails.env.development?
=> true
irb(main):003:0> Rails.env.test?
=> false
RailsにはRailsオブジェクトがあり、それには環境の論理値(boolean)を取るenvという属性がある。
それに、それぞれの環境を渡してクエスチョンで確かめる。
test?ではテスト環境ではtrueを返し、それ以外の環境ではfalseを返す。
テストの環境のデバッグなど、他の環境でconsoleを実行する場合、環境をパラメータとしてconsoleスクリプトに渡すことができる。
$ rails console test
Running via Spring preloader in process 18352
Loading test environment (Rails 5.1.6)
>> Rails.env
=> "test"
>> Rails.env.test?
=> true
Railsサーバーでもデフォルトではdevelopmentが使われるが、他の環境を明示的に実行することもできる。
$ rails s --environment production
なお、アプリケーションを本番環境で実行するには本番環境用のDBが利用できないと実行できないので
$ rails db:migrate RAILS_ENV=production
このように本番データベースを作成する。
ここで、Heroku上での環境を確認してみる。
$ heroku run rails console
irb(main):001:0> Rails.env
=> "production"
irb(main):002:0> Rails.env.production?
=> true
このように、Herokuで実行されるアプリケーションは本番環境であることが分かる。
話を元に戻し、デバッグを整形するスタイルシートを追加
@import "bootstrap-sprockets";
@import "bootstrap";
/* mixins, variables, etc. */
$gray-medium-light: #eaeaea;
@mixin box_sizing { #ミックスイン
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.
.
.
/* miscellaneous */
.debug_dump {
clear: both;
float: left;
width: 100%;
margin-top: 45px;
@include box_sizing;
}
Sassのミックスイン機能(ここではbox_sizing)を使っています。
ミックスイン機能は
@名前 {}
でCSSを定義して
@include 名前
で呼び出すことができる便利なものです
実際にデバック情報をビューでみてみると、
--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
controller: static_pages
action: home
permitted: false
と表示される
これはparamsに含まれている内容で、YAML5 という形式で書かれてる。
YAMLは基本的にハッシュであり、コントローラとページのアクションを一意に指定
演習
ブラウザから /about にアクセスし、デバッグ情報が表示されていることを確認してください。このページを表示するとき、どのコントローラとアクションが使われていたでしょうか?paramsの内容から確認してみましょう。
→動作確認
Railsコンソールを開き、データベースから最初のユーザー情報を取得し、変数userに格納してください。その後、puts user.attributes.to_yamlを実行すると何が表示されますか? ここで表示された結果と、yメソッドを使ったy user.attributesの実行結果を比較してみましょう。
puts user.attributes.to_yamlの場合
irb(main):001:0> user=User.first
(0.1ms) begin transaction
User Load (0.8ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
irb(main):002:0> puts user.attributes.to_yaml
---
id: 1
name: Michael Hartl
email: michael@example.com
created_at: !ruby/object:ActiveSupport::TimeWithZone
utc: &1 2021-02-15 07:32:22.153822000 Z
zone: &2 !ruby/object:ActiveSupport::TimeZone
name: Etc/UTC
time: *1
updated_at: !ruby/object:ActiveSupport::TimeWithZone
utc: &3 2021-02-15 07:32:22.153822000 Z
zone: *2
time: *3
password_digest: "$2a$12$GLOLyYhACS0bU5g25TM9mekWxxEXxmSWumiz03O4tOhy2P4ib5l8K"
=> nil
y user.attributesの場合
irb(main):003:0> y user.attributes
---
id: 1
name: Michael Hartl
email: michael@example.com
created_at: !ruby/object:ActiveSupport::TimeWithZone
utc: &1 2021-02-15 07:32:22.153822000 Z
zone: &2 !ruby/object:ActiveSupport::TimeZone
name: Etc/UTC
time: *1
updated_at: !ruby/object:ActiveSupport::TimeWithZone
utc: &3 2021-02-15 07:32:22.153822000 Z
zone: *2
time: *3
password_digest: "$2a$12$GLOLyYhACS0bU5g25TM9mekWxxEXxmSWumiz03O4tOhy2P4ib5l8K"
=> nil
同じ結果が返ってきました
7.1.2 Usersリソース
6章で1人のユーザーをDBへ登録していたので、その情報をビューに表示する
RESTアーキテクチャの習慣に従い、データの作成、表示、更新、削除をリソース(Resources)として扱い、これらの基本操作を各アクションに割り当てていく
リソースへの参照はリソース名とユニークなIDを使うのが普通
ユーザーをリソースとみなす場合、id=1のユーザーを参照する=/users/1
というURLに対してGETリクエストを発行するということを意味する。(RailsのREST機能が有効になっていると、GETリクエストは自動的にshowアクションとして扱われます。)
/users/1 のURLを有効にするために、routesファイル(config/routes.rb)にresources :users
を追加する
Rails.application.routes.draw do
root 'static_pages#home'
get '/help', to: 'static_pages#help'
get '/about', to: 'static_pages#about'
get '/contact', to: 'static_pages#contact'
get '/signup', to: 'users#new'
resources :users
end
resouresメソッドは以下表の通り名前付きルートとRESTfulなUsersリソースで必要となるすべてのアクションが利用できるようになる
HTTPリクエスト | URL | アクション | 名前付きルート | 用途 |
---|---|---|---|---|
GET | /users | index | users_path | すべてのユーザーを一覧するページ |
GET | /users/1 | show | user_path(user) | 特定のユーザーを表示するページ |
GET | /users/new | new | new_user_path | ユーザーを新規作成するページ(ユーザー登録) |
POST | /users | create | users_path | ユーザーを作成するアクション |
GET | /users/1/edit | edit | edit_user_path(user) | id=1のユーザーを編集するページ |
PATCH | /users/1 | update | user_path(user) | ユーザーを更新するアクション |
DELETE | /users/1 | destroy | user_path(user) | ユーザーを削除するアクション |
ルーティングが有効になったが、ルーティング先のページがないので後ほど追加する
app/views/users/show.html.erb
ファイルを手動で作成し、埋め込みRubyを使ってユーザー名とメールアドレスを表示する
touch app/views/users/show.html.erb
<%= @user.name %>, <%= @user.email %>
ユーザー表示ビューが正常に動作するため,Usersコントローラ内のshowアクションに対応する@user変数を定義する
class UsersController < ApplicationController
def show
@user = User.find(params[:id]) #params[:id]はurlの1の部分
end
def new
end
end
paramsについて
Usersコントローラにリクエストが正常に送信されると、params[:id]の部分はユーザーidの1に置き換わる。このid: '1'は /users/:id から取得した値
つまり、findメソッドのUser.find(1)と同じになる
※params[:id]は文字列型の"1"ですが、findメソッドでは自動的に整数型の1となる
/users/1 にアクセスし、デバッグ情報からparams[:id]の値を確認
action: show
controller: users
id: '1'
演習
埋め込みRubyを使って、マジックカラム(created_atとupdated_at)の値をshowページに表示してみましょう
埋め込みRubyを使って、Time.nowの結果をshowページに表示してみましょう。ページを更新すると、その結果はどう変わっていますか? 確認してみてください
ビューファイルに記述
<%= @user.name %>, <%= @user.email %>
<p><%=@user.created_at%>,<%=@user.updated_at%></p>
<p><%=Time.now%></p>
pタグで挟んだのは改行して見やすくするため
ビューに@userを登録した時間と、updateした時間が表示される
Time.nowは今時点の日にちが表示される(ブラウザを更新すると変わる)
7.1.3 debuggerメソッド
debugメソッドは便利だが、もっと直接的にデバッグする方法もある。
それがdebuggerメソッド
これはbyebug gemによるメソッドで、
コンソールに差し込んで使うことができる
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
debugger
end
def new
end
end
debuggerメソッドを差し込んだら、ブラウザから /users/1 にアクセスし、Railsサーバーを立ち上げたターミナルを確認
[1, 8] in /Users/kiyomasa/environment/sample_app/app/controllers/users_controller.rb
1: class UsersController < ApplicationController
2: def show
3: @user = User.find(params[:id])
4: debugger
=> 5: end
6: def new
7: end
8: end
(byebug)
このプロンプトではRailsコンソールのようにコマンドを呼び出すことができて、アプリケーションのdebuggerが呼び出された瞬間の状態を確認できる
(byebug) @user.name
"Michael Hartl"
(byebug) @user.email
"michael@example.com"
(byebug) params[:id]
"1"
Ctrl-D
を押すとプロンプトから抜け出すことができる
debuggerをUsersコントローラーから取り外す
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
end
def new
end
end
今後Railsアプリケーションの中でよく分からない挙動があったら、debuggerを差し込んで調べてみる。
トラブルが起こっていそうなコードの近くに差し込むのコツ
演習
1.showアクションの中にdebuggerを差し込み(リスト 7.6)、ブラウザから /users/1 にアクセスしてみましょう。その後コンソールに移り、putsメソッドを使ってparamsハッシュの中身をYAML形式で表示してみましょう。
(byebug) puts params[:id].to_yaml
--- '1'
nil
(byebug)
2.newアクションの中にdebuggerを差し込み、/users/new にアクセスしてみましょう。@userの内容はどのようになっているでしょうか? 確認してみてください。
1: class UsersController < ApplicationController
2: def show
3: @user = User.find(params[:id])
4: end
5: def new
6: debugger
=> 7: end
8: end
(byebug) @user
nil
7.1.4 Gravatar画像とサイドバー
Gravatar(Globally Recognized AVATAR)をプロフィールに導入
Gravatarは無料のサービスで、プロフィール写真をアップロードして、指定したメールアドレスと関連付ける
<% provide(:title, @user.name) %>
<h1>
<%= gravatar_for @user %> #これだけでユーザ画像が表示される
<%= @user.name %>
</h1>
このユーザー表示用のビューでは、gravatar_forというヘルパーメソッドを定義している。
@userにはコントローラで定義したように、DBから取り出したユーザー情報が格納されている。
デフォルトでは、ヘルパーファイルで定義されているメソッドは自動的に全てのビューで利用できる。
ここでは、利便性を考えgravatar_forをUsersコントローラに関連付けられているヘルパーファイルに置くことにする。
GravatarのURLはユーザーのメールアドレスをMD5でハッシュ化している。
Rubyでは、Digestライブラリのhexdigestメソッドを使うとMD5のハッシュ化が実現できる。
>> email = "MHARTL@example.COM"
>> Digest::MD5::hexdigest(email.downcase)
=> "1fda4469bcbec3badf5418269ffc5968"
gravatar_forヘルパーメソッドを定義する
module UsersHelper
# 引数で与えられたユーザーのGravatar画像を返す
def gravatar_for(user)
gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}"
image_tag(gravatar_url, alt: user.name, class: "gravatar")
end
end
アプリケーションでGravatarを利用できるようにするために、まずはupdate_attributesを使ってデータベース上のユーザー情報(メールアドレス)を更新
$ rails console
>> user = User.first
>> user.update(name: "Example User",
?> email: "example@railstutorial.org",
?> password: "foobar",
?> password_confirmation: "foobar")
=> true
メアドをexample@railstutorial.orgは既にGravatarでロゴを紐付けしてあるので、更新するとRailsの画像になる
HTMLにサイドバーを追加する
rowクラスとcol-md-4クラスはBootstrapの一部
※親のクラスがrowだと、子クラスでcolが使える。colは全体を12に割った範囲で例えばcol-12なら全体、col-6なら半分の範囲という事になる。col-md-4は画面のwidthがmd(モバイルなど)だと4つのブロックの大きさをとるよという意味になる
<% provide(:title, @user.name) %>
<div class="row">
<aside class="col-md-4">
<section class="user_info">
<h1>
<%= gravatar_for @user %>
<%= @user.name %>
</h1>
</section>
</aside>
</div>
SCSSを使ってサイドバーなどのユーザー表示ページにスタイルを与える
.
.
.
/* sidebar */
aside {
section.user_info {
margin-top: 20px;
}
section {
padding: 10px 0;
margin-top: 20px;
&:first-child {
border: 0;
padding-top: 0;
}
span {
display: block;
margin-bottom: 3px;
line-height: 1;
}
h1 {
font-size: 1.4em;
text-align: left;
letter-spacing: -1px;
margin-bottom: 3px;
margin-top: 0px;
}
}
}
.gravatar {
float: left;
margin-right: 10px;
}
.gravatar_edit {
margin-top: 15px;
}
演習
(任意)Gravatar上にアカウントを作成し、あなたのメールアドレスと適当な画像を紐付けてみてください。メールアドレスをMD5ハッシュ化して、紐付けた画像がちゃんと表示されるかどうか試してみましょう。
7.1.4で定義したgravatar_forヘルパーをリスト 7.12のように変更して、sizeをオプション引数として受け取れるようにしてみましょう。うまく変更できると、gravatar_for user, size: 50といった呼び出し方ができるようになります。重要: この改善したヘルパーは10.3.1で実際に使います。忘れずに実装しておきましょう。
(main):002:0> user.update_attributes(name: "Example user",email: "m23.....@gmail.com
" , password: "foobar", password_confirmation: "foobar" )
=> true
画面に紐付けした自分の画像が表示された
オプション引数は今でもRubyコミュニティで一般的に使われていますが、Ruby 2.0から導入された新機能「キーワード引数(Keyword Arguments)」でも実現することができます。先ほど変更したリスト 7.12を、リスト 7.13のように置き換えてもうまく動くことを確認してみましょう。この2つの実装方法はどういった違いがあるのでしょうか? 考えてみてください。
→単純に置き換えるだけ
7.2 ユーザー登録フォーム
ユーザー登録フォームを作成する
7.2.1 form_withを使用する
ユーザー登録ページで重要な要素は、するフォームです。
Railsでform_withヘルパーメソッドを使い,ユーザー登録に不可欠な情報を入力する。
このメソッドはActive Recordのオブジェクトを取り込み、そのオブジェクトの属性を使ってフォームを構築する。
newアクションに@user変数を追加する
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
end
def new
@user = User.new #@userにUserインスタンスを作成
end
end
ビューを作る前にCSSを追記
.
.
.
/* forms */
input, textarea, select, .uneditable-input {
border: 1px solid #bbb;
width: 100%;
margin-bottom: 15px;
@include box_sizing;
}
input {
height: auto !important;
}
htmlのフォーム側を作成
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_with(model: @user, local: true) do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.label :email %>
<%= f.email_field :email %>
<%= f.label :password %>
<%= f.password_field :password %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation %>
<%= f.submit "Create my account", class: "btn btn-primary" %>
<% end %>
</div>
</div>
演習
試しに、ブロックの変数fをすべてfoobarに置き換えてみて、結果が変わらないことを確認してみてください。確かに結果は変わりませんが、変数名をfoobarとするのはあまり良い変更ではなさそうですね。その理由について考えてみてください。
→繰り返し文の変数みたいなものなのでなんでもいいはず。fの方が簡単だと思う
7.2.2 フォームHTML
フォームのHTMLを分解して解説していく
<%= form_with(model: @user, local: true) do |f| %>
.
.
.
<% end %>
-
do
は、form_with
が1つの変数を持つブロックを取ることを表します。 -
変数fは “form” のfです。
-
ハッシュ引数
local: true
が存在していることに注目 -
form_withはデフォルトで“remote” XHR requestを送信するようだが、ここではエラーメッセージをほぼ確実に表示するために通常の“local”フォームリクエストを送信したいため
-
fオブジェクトは、HTMLフォーム要素(テキストフィールド、ラジオボタン、パスワードフィールドなど)に対応するメソッドが呼び出されると、@userの属性を設定するために特別に設計されたHTMLを返す。つまり、次のコードを実行すると、
<%= f.label :name %>
<%= f.text_field :name %>
Userモデルのname属性を設定する、ラベル付きテキストフィールド要素を作成するのに必要なHTMLを作成する
HTMLソースを確認すると意図するところがわかる
<form accept-charset="UTF-8" action="/users" class="new_user"
id="new_user" method="post">
<input name="authenticity_token" type="hidden"
value="NNb6+J/j46LcrgYUC60wQ2titMuJQ5lLqyAbnbAUkdo=" />
<label for="user_name">Name</label>
<input id="user_name" name="user[name]" type="text" />
<label for="user_email">Email</label>
<input id="user_email" name="user[email]" type="email" />
<label for="user_password">Password</label>
<input id="user_password" name="user[password]"
type="password" />
<label for="user_password_confirmation">Confirmation</label>
<input id="user_password_confirmation"
name="user[password_confirmation]" type="password" />
<input class="btn btn-primary" name="commit" type="submit"
value="Create my account" />
</form>
コードとHTMLソースを比べてみる
<%= f.label :name %>
<%= f.text_field :name %>
↓↓↓
<label for="user_name">Name</label>
<input id="user_name" name="user[name]" type="text" />
lavel
を作成しtype="text"
のinputタグ
を生成している
<%= f.label :email %>
<%= f.email_field :email %>
↓↓
<label for="user_email">Email</label>
<input id="user_email" name="user[email]" type="email" />
<%= f.label :password %>
<%= f.password_field :password %>
↓↓
<label for="user_password">Password</label>
<input id="user_password" name="user[password]" type="password" />
最も重要なのはname属性の使い方
- Railsはnameの値を使って、初期化したハッシュを(params変数経由で)構成する
- このハッシュは、入力された値に基づいてユーザーを作成するときに使われる。
<input id="user_name" name="user[name]" - - - />
.
.
.
<input id="user_password" name="user[password]" - - - />
次に重要な要素は、formタグ自身
- Railsはformタグを作成するときに@userオブジェクトを使う。
- すべてのRubyオブジェクトは自分のクラスを知っているので、Railsは@userのクラスがUserであることを認識
- また、@userは新しいユーザーなので、 Railsはpostメソッドを使ってフォームを構築すべきだと判断。
なお、新しいオブジェクトを作成するために必要なHTTPリクエストはPOSTなので、このメソッドはRESTfulアーキテクチャとして正しいリクエストになる
<form action="/users" class="new_user" id="new_user" method="post">
action="/users"とmethod="post"で、/users に対してHTTPのPOSTリクエスト送信する、といった指示をしている
formタグの内側で次のようなHTMLが生成されている
<input name="authenticity_token" type="hidden"
value="NNb6+J/j46LcrgYUC60wQ2titMuJQ5lLqyAbnbAUkdo=" />
このコードはブラウザ上では何も表示しないが、Railsの内部で使われる特別なコード
簡単にいうとCross-Site Request Forgery(CSRF)と呼ばれる攻撃を阻止するためにトークンを含めたりしている
演習
『Web基礎編: HTML』ではHTMLをすべて手動で書き起こしていますが、なぜformタグを使わなかったのでしょうか? 理由を考えてみてください。
→railsを使用できなかったため
7.3 ユーザー登録失敗
フォームを理解するにはユーザー登録の失敗の時が最も参考になる、ということで
無効なデータ送信を受け付けるユーザー登録フォームを作成、ユーザー登録フォームを更新してエラーの一覧を表示する。
7.3.1 正しいフォーム
resources :usersをroutes.rbファイルに追加すると自動的にRailsアプリケーションがのRESTfulな以下のURI に応答するようになっている
HTTPリクエスト | URL | アクション | 名前付きルート | 用途 |
---|---|---|---|---|
GET | /users | index | users_path | すべてのユーザーを一覧するページ |
GET | /users/1 | show | user_path(user) | 特定のユーザーを表示するページ |
GET | /users/new | new | new_user_path | ユーザーを新規作成するページ(ユーザー登録) |
POST | /users | create | users_path | ユーザーを作成するアクション |
GET | /users/1/edit | edit | edit_user_path(user) | id=1のユーザーを編集するページ |
PATCH | /users/1 | update | user_path(user) | ユーザーを更新するアクション |
DELETE | /users/1 | destroy | user_path(user) | ユーザーを削除するアクション |
formタグでPOSTリクエストをrailsへ送るということは
<form action="/users" class="new_user" id="new_user" method="post">
このHTMLはPOSTリクエストを/usersというURLに送信する。
- /usersへのPOSTリクエストはcreateアクションに送られる。
- createアクションでフォーム送信を受け取り、
- User.newを使って新しいユーザーオブジェクトを作成
- ユーザーを保存(または保存に失敗)
- 再度の送信用のユーザー登録ページを表示する
という流れで実装していく
createアクションを実装
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
end
def new
@user = User.new
end
def create
@user = User.new(params[:user]) # 実装は終わっていないことに注意!
if @user.save
# 保存の成功をここで扱う。
else
render 'new'
end
end
end
コメントにもあるように、上のコードはまだ実装が完了していませんので注意してください
ここで、デバッグ情報のパラメーターハッシュのuserの部分を見てみる。
"user"=>{"name"=>"",
"email"=>"",
"password"=>"[FILTERED]",
"password_confirmation"=>"[FILTERED]"},
userハッシュの中にname email password password_confirmationという四つのハッシュが入っている。
この構造をhash-of-hashesと言う。
つまり、フォームの内容で送られたそれぞれの値は、createアクションで定義したparamsの[:user]にシンボルで渡されている。
[:user]はuserハッシュで、さらにその中にハッシュが入っていてそれぞれの属性値が渡されるという訳。
そして、上記のキー(nameやemailなど)は、newビューのinputタグにあった属性の値になる。
例えば、emailの場合
<input type="email" name="user[email]" id="user_email">
name
属性を見てみると、user[email]
となっている。
これは、userハッシュ
の中のemailキー
の値を送りますよ、という意味。
そして、
createアクションのparams[:user]にて、emailのキーに値として受け取っている。
(ここ重要)
params[:user]の:userはシンボルなので、データの値を受け取れる点もポイント。
@user = User.new(params[:user])
このようなコードは、アスアサインメントと呼ぶ。
これが実質的には
@user = User.new(name: "Foo Bar", email: "foo@invalid",
password: "foo", password_confirmation: "bar")
このようなコードになっている。(フォームで送られた値を受け取ってるから)
ちなみに、昔のRailsでは
@user = User.new(params[:user])
だけでも動いたが、悪意のあるユーザーによってアプリケーションのデータベースが書き換えられてしまう危険性(これをマスアサインメント脆弱性と言う)があるため、このコードのみだとエラーとし、このエラーを解消するためのコードを次項で追記する。
7.3.2 Strong Parameters
マスアサインメントは、次のように値のハッシュを使ってRubyの変数を初期化するもの
@user = User.new(params[:user]) # 実装は終わっていないことに注意!
この実装は最終形でない理由は
- paramsハッシュ全体を初期化す行為はセキュリティ上、極めて危険だから
- ユーザーが送信したデータをまるごとUser.newに渡す
- UserモデルにWebサイトの管理者であるかどうかを示すadmin属性があるとすると
- admin=’1’という値をparams[:user]の一部に紛れ込ませて渡してしまえば、この属性をtrueにすることができてしまう
- curlなどのコマンドを使えばadmin属性を簡単に紛れ込ますことができてしまう
- するとどのユーザーでもadmin=’1’をWebリクエストに紛れ込ませるだけでWebサイトの管理者権限を奪い取ることができてしまう。
※curlコマンドとは
https://curl.se/
https://qiita.com/yasuhiroki/items/a569d3371a66e365316f
curlコマンドとはサーバから、もしくはサーバへデータ転送を行うコマンド
これをRails4.0ではコントローラ層で、Strong Parametersというテクニックを使うことが推奨されている。
Strong Parametersを使うことで、必須のパラメータと許可されたパラメータを指定することが出来る。
さらに、上のようにparamsハッシュを丸ごと渡すとエラーが発生するので、Railsはデフォルトでマスアサインメントの脆弱性から守られるようになった。
この場合、paramsハッシュでは:user属性を必須とし、名前、メールアドレス、パスワード、パスワードの確認の属性をそれぞれ許可し、それ以外を許可しないようにしたい
それをコードにすると以下のようになる
params.require(:user).permit(:name, :email, :password, :password_confirmation)
# :userシンボルを要求し、permit(=許された属性だけ)を要求する =admin属性やその他は遮断する
このコードの戻り値は、許可された属性のみが含まれたparamsのハッシュ(:user属性がない場合はエラー)
これらのパラメータを使いやすくするために、user_params
という外部メソッドを使うのが慣習になっていてparams[:user]の代わりとして使われる
@user = User.new(user_params)
# 上記のコードをuser_paramsを別の場所の定義
最終的にこのようなコードになる
private以下のコードはrails consoleなどでも呼び出しできないよう隠してある
class UsersController < ApplicationController
.
.
.
def create
@user = User.new(user_params) #privete以下で定義したメソッド呼び出し
if @user.save
# 保存の成功をここで扱う。
else
render 'new'
end
end
private
def user_params #指定のparams以外受け取らない機能
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end
privateキーワード以降のコードを強調するために、user_paramsのインデントを1段深くしてある
これで、フォームに何も値を入力しないで送信ボタンを押すと、createアクションにて/usersに飛ぶ
ユーザー登録フォームは動いているが
- 間違った値でも送信出来る
- 新しいユーザーが作成されない
この二つの問題点が発生しているため、これを改善していく。
演習
/signup?admin=1 にアクセスし、paramsの中にadmin属性が含まれていることをデバッグ情報から確認してみましょう。
--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
admin: '1'
controller: users
action: new
permitted: false
7.3.3 エラーメッセージ
ユーザー登録に失敗した場合にエラーメッセージを表示する
例えばユーザー情報のメールアドレスが無効で、パスワードが短すぎる状態で保存しようした場合
$ rails console
>> user = User.new(name: "Foo Bar", email: "foo@invalid",
?> password: "dude", password_confirmation: "dude")
>> user.save
=> false
>> user.errors.full_messages
=> ["Email is invalid", "Password is too short (minimum is 6 characters)"]
user.errors.full_messages
で自動でエラーメッセージが作れる
errors.full_messagesオブジェクト
は、 エラーメッセージの配列を持っている
このメッセージをブラウザで表示するには、ユーザーのnewページでエラーメッセージのパーシャル(partial)を出力
このとき、form-controlというCSSクラスも一緒に追加することで、Bootstrapがスタイルをいい感じにしてくれる。
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_with(model: @user, local: true) do |f| %>
<%= render 'shared/error_messages' %> #パーシャルをrenderしている
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Create my account", class: "btn btn-primary" %>
<% end %>
</div>
</div>
エラーメッセージのような複数のビューで使われるパーシャルは専用のディレクトリ「shared」によく置かれるらしい
今はまだapp/views/sharedといったディレクトリは作っていないので、新しくディレクトリを作成する
$ mkdir app/views/shared
パーシャル(_error_messages.html.erb)も作成
touch app/views/shared/_error_messages.html.erb
error_messages.html.erbに以下コードを追記
<% if @user.errors.any? %> #解説2
<div id="error_explanation"> #解説4
<div class="alert alert-danger">
The form contains <%= pluralize(@user.errors.count, "error") %>. #解説1count #解説3 pluralize
</div>
<ul>
<% @user.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
解説1countメソッド = エラーの数を返す
>> user.errors.count
=> 2
解説2 any?メソッド empty?メソッドの真逆の機能
>> user.errors.empty?
=> false
>> user.errors.any?
=> true
any?メソッド要素が1つでもある場合はtrue、ない場合はfalseを返す
解説3 pluralizeテキストヘルパー
pluralizeの最初の引数に整数が与えられると、それに基づいて2番目の引数の英単語を複数形に変更したものを返す。
このメソッドの背後には強力なインフレクター(活用形生成)があり、不規則活用を含むさまざまな単語を複数形にすることができます。
>> helper.pluralize(1, "error")
=> "1 error"
>> helper.pluralize(5, "error")
=> "5 errors"
>> helper.pluralize(2, "woman")
=> "2 women"
>> helper.pluralize(3, "erratum")
=> "3 errata"
pluralizeを使うことで、コードは次のようになり
<%= pluralize(@user.errors.count, "error") %>
"1 errors" のような英語の文法に合わない文字列を避けることができる
解説4
<div id="error_explanation">
新たにCSSを追加できる
またRailsは、無効な内容の送信によって元のページに戻されると、CSSクラスfield_with_errorsを持ったdivタグでエラー箇所を自動的に囲んでくれる。
これは、newビューで定義したform_forタグで囲んだ中のフィールドの部分を、
自動でfield_with_errorsというdivタグで囲むというもの。
divタグで囲んでくれるためエラーメッセージをSCSSで整形することができる。
Sassの@extend関数を使ってBootstrapのhas-errorというCSSクラスを適用
.
.
.
/* forms */
.
.
.
# error_explanation {
color: red;
ul {
color: red;
margin: 0 0 30px 0;
}
}
.field_with_errors {
@extend .has-error;
.form-control {
color: $state-danger-text;
}
}
今時点、presence: true
によるバリデーションも、has_secure_password
によるバリデーションも
空のパスワード(nil)を検知してしまうため、ユーザー登録フォームで空のパスワードを入力すると2つの同じエラーメッセージが表示されてしまっている。
エラーメッセージを直接修正することも可能だが、後ほど追加するallow_nil: trueというオプションで解決する。
演習
1最小文字数を5に変更すると、エラーメッセージも自動的に更新されることを確かめてみましょう。
validates :password, presence: true, length: { minimum: 5 }
エラーメッセージも変わる
2未送信のユーザー登録フォーム(図 7.13)のURLと、送信済みのユーザー登録フォーム(図 7.19)のURLを比べてみましょう。なぜURLは違っているのでしょうか? 考えてみてください。
ルーティングがビューを表示するURL(new)と、データを保存するURL(create)が異なるため
7.3.4 失敗時のテスト
昔はフォームのテストは毎回手動で行う必要があったが、
Railsではフォーム用テストを書くことができ、こういったプロセスを自動化できる。
今回は無効な送信をした時の正しい振る舞いについてテストを書いていく。
まずは、新規ユーザー登録用(signup)の統合テストを生成する。
$ rails generate integration_test users_signup
invoke test_unit
create test/integration/users_signup_test.rb
このテストでは、ユーザー登録ボタンを押したときに(ユーザー情報が無効であるために)ユーザーが作成されないことを確認する
これを確認するために、ユーザーの数をカウント(count)します。このテストの背後で動作するcountメソッドは、Userを含むあらゆるActive Recordクラスで使うことができます。
$ rails console
>> User.count
=> 1
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
test "invalid signup information" do
get signup_path #newページを取得
assert_no_difference 'User.count' do 以下の動作をして、Userのcountが変わらないことを確認
post users_path, params: { user: { name: "", #createアクションにparamsを送信
email: "user@invalid", #名前もメールもパスワードも適当
password: "foo",
password_confirmation: "bar" } }
end
assert_template 'users/new' #users/newを描写されるか
end
end
テストはパスするはず
演習
リスト 7.20で実装したエラーメッセージに対するテストを書いてみてください。どのくらい細かくテストするかはお任せします。リスト 7.25にテンプレートを用意しておいたので、参考にしてください。
assert_template 'users/new'
assert_select 'div#error_explanation' # cssが表示されればOK
assert_select 'div.field_with_errors' #cssが表示されればOK =エラーが表示されている証明
end
.
.
.
end
7.4 ユーザー登録成功
- フォームが有効な場合に新規ユーザーを実際にデータベースに保存できるようにする
- 保存に成功すると、ユーザー情報は自動的にデータベースに登録
- 次にブラウザの表示をリダイレクトして、登録されたユーザーのプロフィールを表示
- ついでにウェルカムメッセージも表示
7.4.1 登録フォームの完成
保存成功時の動作を書いていく。
現時点では、有効な情報でフォーム送信してもエラーが発生してしまう。
これは、データをcreateアクションに送った際、対応するビューがないことが原因。
確かにビューを作成すればエラーは解消されるが、新たなビューは作らず、redirect_toメソッドを使い、保存成功と同時に/usersへ移動させる。
class UsersController < ApplicationController
.
def create
@user = User.new(user_params)
if @user.save
redirect_to @user #userのshowを描写
else
render 'new'
end
end
.
.
end
redirect_to @user
は次のコードと等価
redirect_to user_url(@user)
これはredirect_to @userというコードから(Railsエンジニアが)user_url(@user)といったコードを実行したいということを、Railsが推察してくれた結果
演習
1有効な情報を送信し、ユーザーが実際に作成されたことを、Railsコンソールを使って確認してみましょう。
irb(main):001:0> User.find_by(email: "aaaa@com.com")
=> #<User id: 2, name: "inoue", email: "aaaa@com.com", created_at: "2021-02-17 06:02:39", updated_at: "2021-02-17 06:02:39", password_digest: [FILTERED]>
登録されている
2リスト 7.26を更新し、redirect_to user_url(@user)とredirect_to @userが同じ結果になることを確認してみましょう。
確認済
7.4.2 flash
flashという特殊な変数を使い、登録完了後に表示されるページにフラッシュメッセージを表示する
Railsの一般的な慣習に倣って、:successというキーには成功時のメッセージを代入するようにする
class UsersController < ApplicationController
.
.
.
def create
@user = User.new(user_params)
if @user.save
flash[:success] = "Welcome to the Sample App!" #flashメッセージを表示
redirect_to @user
else
render 'new'
end
end
end
flash変数に代入したメッセージは、リダイレクトした直後のページで表示できるようになる
ビューで表示する前にコンソールで挙動を確認
$ rails console
>> flash = { success: "It worked!", danger: "It failed." }#flashにsuccessとdangerキー、メッセを追加
=> {:success=>"It worked!", danger: "It failed."}
>> flash.each do |key, value| #繰り返し文でflashのハッシュを当てはめていく
?> puts "#{key}"
?> puts "#{value}"
>> end
success
It worked!
danger
It failed.
レイアウトを追加
<!DOCTYPE html>
<html>
.
.
.
<body>
<%= render 'layouts/header' %>
<div class="container">
<% flash.each do |message_type, message| %> #解説1
<div class="alert alert-<%= message_type %>"><%= message %></div>
<% end %>
<%= yield %>
<%= render 'layouts/footer' %>
<%= debug(params) if Rails.env.development? %>
</div>
.
.
.
</body>
</html>
解説1
<% flash.each do |message_type, message| %>
<div class="alert alert-<%= message_type %>"><%= message %></div>
<% end %>
alert-<%= message_type %>
適用するCSSクラスをメッセージの種類によって変更するようにしている。
例えば:successキーのメッセージが表示される場合、alert-success
というクラスになる
flashの:successキー
はシンボルなのになぜ、そうなるかというと
埋め込みRubyが自動的に"success"という文字列に変換する
この性質を利用して、キーの内容によって異なったCSSクラスを適用させることができ、メッセージの種類によってスタイルを動的に変更させることができるようになる
例えばflash[:danger]を使ってログインに失敗したことを表すメッセージを表示したい場合
Bootstrap CSSは、このようなflashのクラス用に4つのスタイルを持っていて(success、info、warning、danger)、スタイルを場合に応じて使う
最終的には次のようなHTMLになる
<div class="alert alert-success">Welcome to the Sample App!</div>
演習
1コンソールに移り、文字列内の式展開(4.2.1)でシンボルを呼び出してみましょう。例えば"#{:success}"といったコードを実行すると、どんな値が返ってきますか? 確認してみてください。
irb(main):002:0> "#{:success}"
=> "success"
2先ほどの演習で試した結果を参考に、リスト 7.28のflashはどのような結果になるか考えてみてください。
同じことをする
flash = { success: "It worked!", danger: "It failed." }
=> {:success=>"It worked!", danger: "It failed."}
>> flash.each do |key, value|
?> puts "#{key}"
?> puts "#{value}"
>> end
success
It worked!
danger
It failed.
7.4.3 実際のユーザー登録
実際にユーザー登録を試す
ユーザー登録を試す前に、次のコマンドを実行してデータベースの内容を一旦リセット
$ rails db:migrate:reset
環境によっては、ここでWebサーバーを再起動する
今回は名前を「Rails Tutorial」
メールアドレスを「example@railstutorial.org」として登録
→実際にできた
演習
1Railsコンソールを使って、新しいユーザーが本当に作成されたのかもう一度チェックしてみましょう。結果は、リスト 7.30のようになるはずです。
2自分のメールアドレスでユーザー登録を試してみましょう。既にGravatarに登録している場合、適切な画像が表示されているか確認してみてください。
→登録済
7.4.4 成功時のテスト
テストを書いていく
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
.
.
.
test "valid signup information" do
get signup_path
assert_difference 'User.count', 1 do #Userのカウントが1増えるか
post users_path, params: { user: { name: "Example User", #正しいユーザをparamsに渡す
email: "user@example.com",
password: "password",
password_confirmation: "password" } }
end
follow_redirect! #postリクエストを送信した結果をみて指定されたリダイレクト先に移動する
assert_template 'users/show'
end
end
assert_template 'users/show'
演習
1 7.4.2で実装したflashに対するテストを書いてみてください。どのくらい細かくテストするかはお任せします。リスト 7.32に最小限のテンプレートを用意しておいたので、参考にしてください(FILL_INの部分を適切なコードに置き換えると完成します)。ちなみに、テキストに対するテストは壊れやすいです。文量の少ないflashのキーであっても、それは同じです。筆者の場合、flashが空でないかをテストするだけの場合が多いです。
リスト 7.32: flashをテストするためのテンプレート
require 'test_helper'
.
.
.
test "valid signup information" do
get signup_path
assert_difference 'User.count', 1 do
post users_path, params: { user: { name: "Example User",
email: "user@example.com",
password: "password",
password_confirmation: "password" } }
end
follow_redirect!
assert_template 'users/show'
assert_not flash.FILL_IN
assert_not flash.blank?
end
end
2本文中でも指摘しましたが、flash用のHTML(リスト 7.29)は読みにくいです。より読みやすくしたリスト 7.33のコードに変更してみましょう。変更が終わったらテストスイートを実行し、正常に動作することを確認してください。なお、このコードでは、Railsのcontent_tagというヘルパーを使っています。
リスト 7.33: content_tagを使ってレイアウトの中にflashを埋め込む
<!DOCTYPE html>
<html>
<% flash.each do |message_type, message| %>
<%= content_tag(:div, message, class: "alert alert-#{message_type}") %>
<% end %>
.
</html>
書き換えて問題なくtestはパスする
3リスト 7.26のリダイレクトの行をコメントアウトすると、テストが失敗することを確認してみましょう。
FAIL["test_valid_signup_information", #<Minitest::Reporters::Suite:0x00007ffb9e0d73b0 @name="UsersSignupTest">, 1.6894480008631945]
test_valid_signup_information#UsersSignupTest (1.69s)
expecting <"users/show"> but rendering with <[]>
test/integration/users_signup_test.rb:26:in `block in <class:UsersSignupTest>'
4リスト 7.26で、@user.saveの部分をfalseに置き換えたとしましょう(バグを埋め込んでしまったと仮定してください)。このとき、assert_differenceのテストではどのようにしてこのバグを検知するでしょうか? テストコードを追って考えてみてください。
保存失敗だとすると、保存前(get)で読み込んだassert_differenceで、User.countの値が変わらず(1→1)、postでcreateアクションに送るも、paramsでuserハッシュの値を受け取れず失敗する。
7.5 プロのデプロイ
これまでのデプロイと違って、実際に本番環境でデータを操作できるようにする。
具体的には、ユーザー登録をセキュアにするために、本番用アプリケーションに重要な機能を追加していく。
その後、デフォルトのWebサーバーをプロが使うWebサーバーに置き換える
まずはこの時点までの変更をmasterブランチにマージ
$ git add -A
$ git commit -m "Finish user signup"
$ git checkout master
$ git merge sign-up
7.5.1 本番環境でのSSL
これまでと違うのは、ユーザー登録フォームでデータを送信するという点。
ユーザーの名前やパスワードなどがネットワーク越しに流されていく。
このような個人情報は暗号化しないと、ハッカーなんかに情報を盗まれてしまうため非常に危険。
情報セキュリティの三原則「機密性の確保」を行うためにも、Secure Sockets Layer(SSL)を使って、
通信を暗号化する。
SSLを有効化するのは簡単で、production.rbと言う本番環境の設定ファイルに「本番環境ではSSLを使うようにする」1行を修正するだけで済む
Rails.application.configure do
.
.
.
# Force all access to the app over SSL, use Strict-Transport-Security,
# and use secure cookies.
config.force_ssl = true #記述する
.
.
.
end
本番用のWebサイトでSSLを使えるようにするためには、ドメイン毎にSSL証明書を購入し、セットアップする必要がある。
しかし、お金もかかるので、herokuのSSL証明書に便乗する形でセットアップする
7.5.2 本番環境用のWebサーバー
Herokuデフォルでは、Rubyだけで実装されたWEBrickと言うWebサーバーを使っているが、
多くのトラフィックを捌けないので、今回はPumaを使う。
最初にpuma gemをGemfileに追加する必要があるが、Rails 5ではデフォルトの設定でも使えるので、今回は必要なし。
pumaファイルに以下を記述するだけ
デフォルトでコメントアウト記述がたくさんあるが、削除してこれをコピーした方が早い
# Pumaの設定ファイル
max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
threads min_threads_count, max_threads_count
port ENV.fetch("PORT") { 3000 }
environment ENV.fetch("RAILS_ENV") { ENV['RACK_ENV'] || "production" }
pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
workers ENV.fetch("WEB_CONCURRENCY") { 2 }
preload_app!
plugin :tmp_restart
最後に、Procfileと呼ばれる、Heroku上でPumaのプロセスを走らせる設定ファイルを作成
なお、このProcfileはルートディレクトリ(Gemfileと同じディレクトリ)に置いておく必要があるので、ファイルを置くディレクトリには注意してください。
$ touch ./Procfile
web: bundle exec puma -C config/puma.rb
7.5.3 本番データベースを設定する
最後に本番データベースを正しく設定する。
使うデータベースはPostgreSQL
PostgreSQLはHerokuで何の設定を行わなくても動作しますが、HerokuのRails 6向け公式ドキュメントでは明示的にPostgreSQLを設定することを推奨していますので、念のためそれに沿って追記
# SQLite version 3.x
# gem install sqlite3
#
# Ensure the SQLite 3 gem is defined in your Gemfile
# gem 'sqlite3'
#
default: &default
adapter: sqlite3
pool: 5
timeout: 5000
development:
<<: *default
database: db/development.sqlite3
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
database: db/test.sqlite3
production:
adapter: postgresql
encoding: unicode
# For details on connection pooling, see Rails configuration guide
# https://railsguides.jp/configuring.html#データベース接続をプールする
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
database: sample_app_production
username: sample_app
password: <%= ENV['SAMPLE_APP_DATABASE_PASSWORD'] %>
7.5.4 本番環境へのデプロイ
これで、本番環境用のWebサーバーとデータベースの設定は完了
変更をコミットし、デプロイしてみる
$ rails test
$ git add -A
$ git commit -m "Use SSL and the Puma webserver in production"
$ git push && git push heroku
Herokuのデプロイするとき 次のような警告メッセージが出るかもしれないが、今回は気にしなくてOK
###### WARNING:
You have not declared a Ruby version in your Gemfile.
To set your Ruby version add this line to your Gemfile:
ruby '2.6.3'
私は出ました
演習
ブラウザから本番環境(Heroku)にアクセスし、SSLの鍵マークがかかっているか、URLがhttpsになっているかどうかを確認してみましょう。
→確認ずみ
本番環境でユーザーを作成してみましょう。Gravatarの画像は正しく表示されているでしょうか?
→問題なく画像表示された
この章も長かったが覚えることも多かった。