今日やったこと11
第11章 アカウントの有効化
アカウントを有効化するステップを新規登録の途中に差し込むことで、本当にそのメールアドレスの持ち主なのかどうかを確認できるようにする。
アカウントを有効化する手順
1.ユーザーの初期状態は「有効化されていない」(unactivated) にしておく。
2.ユーザー登録が行われたときに、有効化トークンと、それに対応する有効化ダイジェストを生成する。
3.有効化ダイジェストはデータベースに保存しておき、有効化トークンはメールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく。
4.ユーザーがメールのリンクをクリックしたら、アプリケーションはメールアドレスをキーにしてユーザーを探し、データベース内に保存しておいた有効化ダイジェストと比較することでトークンを認証する。
5.ユーザーを認証できたら、ユーザーのステータスを「有効化されていない」から「有効化済み」(activated) に変更する。
11.1 AccountActivationsリソース
新機能用のトピックブランチを作成。
$ git checkout -b account-activation
まずはAccountActivationsコントローラを生成。
$ rails generate controller AccountActivations
ルーティングにアカウント有効化用のresources行を追加。
config/routes.rb 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' get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy' resources :users resources :account_activations, only: [:edit] end
Userモデルに3つの属性を追加する。
$ rails generate migration add_activation_to_users \ > activation_digest:string activated:boolean activated_at:datetime
マイグレーションファイルでactivated属性のデフォルトの論理値をfalseに設定して実行。
db/migrate/[timestamp]_add_activation_to_users.rb class AddActivationToUsers < ActiveRecord::Migration[5.0] def change add_column :users, :activation_digest, :string add_column :users, :activated, :boolean, default: false add_column :users, :activated_at, :datetime end end
$ rails db:migrate
Userモデルにアカウント有効化のコードを追加する。
app/models/user.rb class User < ApplicationRecord attr_accessor :remember_token, :activation_token before_save :downcase_email # オブジェクトが作成されたときだけコールバックを呼び出す。 before_create :create_activation_digest validates :name, presence: true, length: { maximum: 50 } . . . private # メールアドレスをすべて小文字にする def downcase_email self.email = email.downcase end # 有効化トークンとダイジェストを作成および代入する def create_activation_digest self.activation_token = User.new_token self.activation_digest = User.digest(activation_token) end end
サンプルデータとfixtureを更新し、テスト時のサンプルとユーザーを事前に有効化しておく。
db/seeds.rb User.create!(name: "Example User", email: "example@railstutorial.org", password: "foobar", password_confirmation: "foobar", admin: true, activated: true, activated_at: Time.zone.now) 99.times do |n| name = Faker::Name.name email = "example-#{n+1}@railstutorial.org" password = "password" User.create!(name: name, email: email, password: password, password_confirmation: password, activated: true, activated_at: Time.zone.now) end
test/fixtures/users.yml michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> admin: true activated: true activated_at: <%= Time.zone.now %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %> activated: true activated_at: <%= Time.zone.now %> lana: name: Lana Kane email: hands@example.gov password_digest: <%= User.digest('password') %> activated: true activated_at: <%= Time.zone.now %> malory: name: Malory Archer email: boss@example.gov password_digest: <%= User.digest('password') %> activated: true activated_at: <%= Time.zone.now %> <% 30.times do |n| %> user_<%= n %>: name: <%= "User #{n}" %> email: <%= "user-#{n}@example.com" %> password_digest: <%= User.digest('password') %> activated: true activated_at: <%= Time.zone.now %> <% end %>
データベースを初期化して、サンプルデータを再度生成し直し、上記の変更を反映する。
$ rails db:migrate:reset $ rails db:seed
11.2 アカウント有効化のメール送信
Action Mailerライブラリを使ってUserのメイラーを追加する。
メイラーはUsersコントローラのcreateアクションで有効化リンクをメール送信するために使う。
メールのテンプレートの中に有効化トークンとメールアドレス (= 有効にするアカウントのアドレス) のリンクを含めて、ビューと同じ要領で定義できる。
rails generateでメイラーを生成。
$ rails generate mailer UserMailer account_activation password_reset
生成されたメイラーのテンプレートを使えるように変更する。
app/mailers/application_mailer.rb class ApplicationMailer < ActionMailer::Base default from: "noreply@example.com" layout 'mailer' end
app/mailers/user_mailer.rb class UserMailer < ApplicationMailer def account_activation(user) @user = user mail to: user.email, subject: "Account activation" end def password_reset @greeting = "Hi" mail to: "to@example.org" end end
生成されたビューのテンプレートの2つを変更。
app/views/user_mailer/account_activation.text.erb Hi <%= @user.name %>, Welcome to the Sample App! Click on the link below to activate your account: <%= edit_account_activation_url(@user.activation_token, email: @user.email) %>
app/views/user_mailer/account_activation.html.erb <h1>Sample App</h1> <p>Hi <%= @user.name %>,</p> <p> Welcome to the Sample App! Click on the link below to activate your account: </p> <%= link_to "Activate", edit_account_activation_url(@user.activation_token, email: @user.email) %>
演習 11.2.1.1
>> CGI.escape("Don't panic!") => "Don%27t+panic%21"
送信メールのプレビュー
Railsでは、特殊なURLにアクセスするとメールのメッセージをその場でプレビューすることができる。これを利用するために、アプリケーションのdevelopment環境の設定に手を加える。
config/environments/development.rb Rails.application.configure do . . . config.action_mailer.raise_delivery_errors = true config.action_mailer.delivery_method = :test host = 'localhost:3000' # 自分の環境に合わせて変更する。 config.action_mailer.default_url_options = { host: host, protocol: 'https' } . . . end
developmentサーバーを再起動して上記の設定を読み込み、自動生成したUserメイラーのプレビューファイルを更新する。
test/mailers/previews/user_mailer_preview.rb # Preview all emails at http://localhost:3000/rails/mailers/user_mailer class UserMailerPreview < ActionMailer::Preview # Preview this email at # http://localhost:3000/rails/mailers/user_mailer/account_activation def account_activation user = User.first user.activation_token = User.new_token UserMailer.account_activation(user) end # Preview this email at # http://localhost:3000/rails/mailers/user_mailer/password_reset def password_reset UserMailer.password_reset end end
下記のURLでアカウント有効化メールの内容を確認。
http://localhost:3000/rails/mailers/user_mailer/account_activation http://localhost:3000/rails/mailers/user_mailer/account_activation.txt
Railsによる自動生成された送信メールのテストを変更。
test/mailers/user_mailer_test.rb require 'test_helper' class UserMailerTest < ActionMailer::TestCase test "account_activation" do user = users(:michael) user.activation_token = User.new_token mail = UserMailer.account_activation(user) assert_equal "Account activation", mail.subject assert_equal [user.email], mail.to assert_equal ["noreply@example.com"], mail.from assert_match user.name, mail.body.encoded assert_match user.activation_token, mail.body.encoded assert_match CGI.escape(user.email), mail.body.encoded end end
テストファイル内のドメイン名を正しく設定する。
config/environments/test.rb Rails.application.configure do . . . config.action_mailer.delivery_method = :test config.action_mailer.default_url_options = { host: 'example.com' } . . . end
ユーザーのcreateアクションを更新する。
app/controllers/users_controller.rb class UsersController < ApplicationController . . . def create @user = User.new(user_params) if @user.save UserMailer.account_activation(@user).deliver_now flash[:info] = "Please check your email to activate your account." redirect_to root_url else render 'new' end end . . . end
11.3 アカウントを有効化する
authenticated?メソッドを抽象化する。
app/models/user.rb class User < ApplicationRecord . . . # トークンがダイジェストと一致したらtrueを返す def authenticated?(attribute, token) digest = send("#{attribute}_digest") return false if digest.nil? BCrypt::Password.new(digest).is_password?(token) end . . .
editアクションでユーザーを有効化する。
app/controllers/account_activations_controller.rb class AccountActivationsController < ApplicationController def edit user = User.find_by(email: params[:email]) if user && !user.activated? && user.authenticated?(:activation, params[:id]) user.update_attribute(:activated, true) user.update_attribute(:activated_at, Time.zone.now) log_in user flash[:success] = "Account activated!" redirect_to user else flash[:danger] = "Invalid activation link" redirect_to root_url end end end
ユーザーの有効化を有効的につかい、ユーザーが有効である場合にのみログインできるようにログイン方法を変更する。
app/controllers/sessions_controller.rb class SessionsController < ApplicationController def new end def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) if user.activated? log_in user params[:session][:remember_me] == '1' ? remember(user) : forget(user) redirect_back_or user else message = "Account not activated. " message += "Check your email for the activation link." flash[:warning] = message redirect_to root_url end else flash.now[:danger] = 'Invalid email/password combination' render 'new' end end def destroy log_out if logged_in? redirect_to root_url end end
アカウント有効化の統合テストを追加する。
test/integration/users_signup_test.rb require 'test_helper' class UsersSignupTest < ActionDispatch::IntegrationTest def setup ActionMailer::Base.deliveries.clear # deliveriesは変数なので、setupメソッドでこれを初期化 end test "invalid signup information" do get signup_path assert_no_difference 'User.count' do post users_path, params: { user: { name: "", email: "user@invalid", password: "foo", password_confirmation: "bar" } } end assert_template 'users/new' assert_select 'div#error_explanation' assert_select 'div.field_with_errors' end test "valid signup information with account activation" 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 assert_equal 1, ActionMailer::Base.deliveries.size user = assigns(:user) #assignsメソッドを使うと対応するアクション内のインスタンス変数にアクセスできる assert_not user.activated? # 有効化していない状態でログインしてみる log_in_as(user) assert_not is_logged_in? # 有効化トークンが不正な場合 get edit_account_activation_path("invalid token", email: user.email) assert_not is_logged_in? # トークンは正しいがメールアドレスが無効な場合 get edit_account_activation_path(user.activation_token, email: 'wrong') assert_not is_logged_in? # 有効化トークンが正しい場合 get edit_account_activation_path(user.activation_token, email: user.email) assert user.reload.activated? follow_redirect! assert_template 'users/show' assert is_logged_in? end end
リファクタリングのため、Userモデルにユーザー有効化メソッドを追加する。
activateメソッドはユーザーの有効化属性を更新し、send_activation_emailメソッドは有効化メールを送信する。
app/models/user.rb class User < ApplicationRecord . . . # アカウントを有効にする def activate update_attribute(:activated, true) update_attribute(:activated_at, Time.zone.now) end # 有効化用のメールを送信する def send_activation_email UserMailer.account_activation(self).deliver_now end private . . . end
作成したメソッドを各アクションで使用する。
app/controllers/users_controller.rb class UsersController < ApplicationController . . . def create @user = User.new(user_params) if @user.save @user.send_activation_email # send_activation_emailメソッド flash[:info] = "Please check your email to activate your account." redirect_to root_url else render 'new' end end . . . end
app/controllers/account_activations_controller.rb class AccountActivationsController < ApplicationController def edit user = User.find_by(email: params[:email]) if user && !user.activated? && user.authenticated?(:activation, params[:id]) user.activate # activateメソッド log_in user flash[:success] = "Account activated!" redirect_to user else flash[:danger] = "Invalid activation link" redirect_to root_url end end end
rails server実行時にエラーがでて起動しない
rails s
上記のコマンドを実行
ec2-user:~/environment/sample_app (updateing-users) $ rails s => Booting Puma => Rails 5.1.4 application starting in development => Run `rails server -h` for more startup options [12158] Puma starting in cluster mode... [12158] * Version 3.9.1 (ruby 2.4.1-p111), codename: Private Caller [12158] * Min threads: 5, max threads: 5 [12158] * Environment: development [12158] * Process workers: 2 [12158] * Preloading application [12158] * Listening on tcp://localhost:8080 Exiting
といったログが出てサーバが起動しなくなりました
コマンドでプロセスを確認してみると
ps aux | grep puma
pumaのプロセスが2つあるのがいけないのかな
ec2-user 4090 0.0 11.2 875716 114244 ? Sl 00:45 0:01 puma: cluster worker 0: 4081 [sample_app] ec2-user 4997 0.0 17.0 939724 172476 ? Sl 01:52 0:03 puma: cluster worker 1: 4081 [sample_app] ec2-user 12324 0.0 0.2 110520 2072 pts/1 S+ 05:31 0:00 grep --color=auto puma
一応killコマンドで2つとも排除します
kill -9 4090 kill -9 4997
rails s
再び上記コマンドを実行して無事起動できました
今日やったこと10
第10章ユーザーの更新・表示・削除 続きから
フレンドリーフォワーディングを実装する。
ユーザーを希望のページに転送するようにさせるため、リクエスト時点のページをどこかに保存しておき、その場所にリダイレクトさせる必要がある。
Sessionsヘルパーでstore_locationとredirect_back_orの2つのメソッドを定義して使用する。
app/helpers/sessions_helper.rb module SessionsHelper . . . # 記憶したURL (もしくはデフォルト値) にリダイレクト def redirect_back_or(default) # リクエストされたURLが存在する場合はそこにリダイレクトし、ない場合は何らかのデフォルトのURLにリダイレクト redirect_to(session[:forwarding_url] || default) # 転送用のURLを削除 session.delete(:forwarding_url) end # アクセスしようとしたURLを覚えておく def store_location # GETリクエストが送られたときだけrequest.original_urlでリクエスト先を取得しsessionに保存 session[:forwarding_url] = request.original_url if request.get? end end
app/controllers/users_controller.rb def logged_in_user unless logged_in? store_location #ログインユーザー用beforeフィルターにstore_locationを追加 flash[:danger] = "Please log in." redirect_to login_url end end
app/controllers/sessions_controller.rb def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) log_in user params[:session][:remember_me] == '1' ? remember(user) : forget(user) redirect_back_or user #redirect_back_orを追加 else flash.now[:danger] = 'Invalid email/password combination' render 'new' end end
10.3 すべてのユーザーを表示する
indexアクションを追加し、すべてのユーザーを一覧表示する。
ユーザーのshowページについては、ログインしているかどうかに関わらずサイトを訪れたすべてのユーザーから見えるようにするが、ユーザーのindexページはログインしたユーザーにしか見せないようにして、未登録のユーザーがデフォルトで表示できるページを制限する。
indexページを不正なアクセスから守るために、まずはindexアクションが正しくリダイレクトするか検証するテストを書く。
test/controllers/users_controller_test.rb require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) @other_user = users(:archer) end # indexアクションのリダイレクトをテスト test "should redirect index when not logged in" do get users_path assert_redirected_to login_url end . . . end
次に、beforeフィルターのlogged_in_userにindexアクションを追加して、このアクションを保護する。
app/controllers/users_controller.rb class UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update] <== before_action :correct_user, only: [:edit, :update] def index end . . . end
すべてのユーザーを表示するために、全ユーザーが格納された変数を作成し、順々に表示するindexビューを実装する。
def index @users = User.all end
app/views/users/index.html.erb <% provide(:title, 'All users') %> <h1>All users</h1> <ul class="users"> <% @users.each do |user| %> <li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> </li> <% end %> </ul>
CSSでレイアウトを調整し、ヘッダーにユーザー一覧表示用のリンクを追加する。
app/assets/stylesheets/custom.scss . . . /* Users index */ .users { list-style: none; margin: 0; li { overflow: auto; padding: 10px 0; border-bottom: 1px solid $gray-lighter; } }
app/views/layouts/_header.html.erb <header class="navbar navbar-fixed-top navbar-inverse"> <div class="container"> <%= link_to "sample app", root_path, id: "logo" %> <nav> <ul class="nav navbar-nav navbar-right"> <li><%= link_to "Home", root_path %></li> <li><%= link_to "Help", help_path %></li> <% if logged_in? %> <li><%= link_to "Users", users_path %></li> #ユーザー一覧ページへのリンクを追加 <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown"> Account <b class="caret"></b> </a> <ul class="dropdown-menu"> <li><%= link_to "Profile", current_user %></li> <li><%= link_to "Settings", edit_user_path(current_user) %></li> <li class="divider"></li> <li> <%= link_to "Log out", logout_path, method: :delete %> </li> </ul> </li> <% else %> <li><%= link_to "Log in", login_path %></li> <% end %> </ul> </nav> </div> </header>
演習 10.3.1.1
test/integration/site_layout_test.rb require 'test_helper' class SiteLayoutTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end ・ ・ ・ # ログイン時のレイアウトリンクに対するテスト test "layout links when logged in" do log_in_as(@user) get root_path assert_template 'static_pages/home' assert_select "a[href=?]", users_path assert_select "a[href=?]", user_path(@user) assert_select "a[href=?]", edit_user_path(@user) assert_select "a[href=?]", logout_path end end
サンプルのユーザーを追加する。
まず、GemfileにFaker gem(実際にいそうなユーザー名を作成するgem)を追加する。
gem 'faker', '1.7.3'
bundle installを実行
$ bundle install
サンプルユーザーを生成するRubyスクリプト (Railsタスクとも呼ぶ) を追加ためにRailsではdb/seeds.rbというファイルを標準として使う。
db/seeds.rb User.create!(name: "Example User", email: "example@railstutorial.org", password: "foobar", password_confirmation: "foobar") 99.times do |n| name = Faker::Name.name email = "example-#{n+1}@railstutorial.org" password = "password" User.create!(name: name, email: email, password: password, password_confirmation: password) end
データベースをリセットして、Railsタスクを実行。
$ rails db:migrate:reset $ rails db:seed
ページネーションを使い1つのページに表示するユーザーの数を制限する。
Railsには豊富なページネーションメソッドがあり、その中のwill_paginateメソッドを使う。
Bootstrapのページネーションスタイルも使いたいので、
will_paginate gem とbootstrap-will_paginate gemをGemfileに追加する。
gem 'will_paginate', '3.1.6' gem 'bootstrap-will_paginate', '1.0.0'
bundle installを実行
$ bundle install
ページネーションを動作させるため、ユーザーのページネーションを行うようにRailsに指示するwill_paginateメソッドををindexビューに追加する。
app/views/users/index.html.erb <% provide(:title, 'All users') %> <h1>All users</h1> <%= will_paginate %> <ul class="users"> <% @users.each do |user| %> <li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> </li> <% end %> </ul> <%= will_paginate %>
will_paginateではpaginateメソッドを使った結果が必要なので
indexアクション内のallをpaginateメソッドに置き換える。
ここで:pageパラメーターにはparams[:page]が使われているが、これはwill_paginateによって自動的に生成されたものである。
def index @users = User.paginate(page: params[:page]) end
ページネーションに対するテストを書いていく。
テスト用のデータベースに31人以上のユーザーがいる必要があるのでfixtureファイルに追加する。
なお、今後必要になるので、2人の名前付きユーザーも一緒に追加しておく。
test/fixtures/users.yml michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %> lana: name: Lana Kane email: hands@example.gov password_digest: <%= User.digest('password') %> malory: name: Malory Archer email: boss@example.gov password_digest: <%= User.digest('password') %> <% 30.times do |n| %> user_<%= n %>: name: <%= "User #{n}" %> email: <%= "user-#{n}@example.com" %> password_digest: <%= User.digest('password') %> <% end %>
indexページに対するテストのファイルを生成しコードを作成する。
$ rails generate integration_test users_index invoke test_unit create test/integration/users_index_test.rb
test/integration/users_index_test.rb require 'test_helper' class UsersIndexTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end # ページネーションを含めたUsersIndexのテスト test "index including pagination" do log_in_as(@user) #ログイン get users_path #indexページにアクセス assert_template 'users/index' #最初のページにユーザーがいることを確認 assert_select 'div.pagination' #ページネーションのリンクがあることを確認 User.paginate(page: 1).each do |user| assert_select 'a[href=?]', user_path(user), text: user.name end end end
各ユーザーを表示するパーシャルを作りindexページをリファクタリングする
app/views/users/index.html.erb <% provide(:title, 'All users') %> <h1>All users</h1> <%= will_paginate %> <ul class="users"> <%= render @users %> </ul> <%= will_paginate %>
app/views/users/_user.html.erb <li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> </li>
10.4 ユーザーを削除する
削除を実行できる権限を持つ管理 (admin) ユーザーを識別するために、論理値をとるadmin属性をUserモデルに追加する。
属性の型をbooleanと指定しマイグレーションを実行する。
$ rails generate migration add_admin_to_users admin:boolean
$ rails db:migrate
admin属性が追加されて値はデフォルトでfalseとなり、疑問符の付いたadmin?メソッドも利用できるようになる。
>> user.admin? => false
最初のユーザーだけをデフォルトで管理者にするようサンプルデータを更新し、
データベースをリセットして、サンプルデータを再度生成する。
b/seeds.rb User.create!(name: "Example User", email: "example@railstutorial.org", password: "foobar", password_confirmation: "foobar", admin: true) 99.times do |n| name = Faker::Name.name email = "example-#{n+1}@railstutorial.org" password = "password" User.create!(name: name, email: email, password: password, password_confirmation: password) end
$ rails db:migrate:reset $ rails db:seed
演習 10.4.1.1
test/controllers/users_controller_test.rb require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) @other_user = users(:archer) end . . . # admin属性の変更が禁止されていることをテスト test "should not allow the admin attribute to be edited via the web" do log_in_as(@other_user) assert_not @other_user.admin? patch user_path(@other_user), params: { user: { password: @other_user, password_confirmation: @other_user, admin: true } } assert_not @other_user.reload.admin? end . . . end
ユーザーindexページの各ユーザーに削除用のリンクを追加し、続いて管理ユーザーへのアクセスを制限する。
これによって、現在のユーザーが管理者のときに限り [delete] リンクが表示されるようになる。
app/views/users/_user.html.erb <li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> <% if current_user.admin? && !current_user?(user) %> | <%= link_to "delete", user, method: :delete, data: { confirm: "You sure?" } %> <% end %> </li>
削除リンクを動作させるために、destroyアクションを追加する。
このアクションでは、該当するユーザーを見つけてActive Recordのdestroyメソッドを使って削除し、最後にユーザーindexに移動するようにする。
ユーザーを削除するためにはログインしていなくてはならないので、destroyアクションもlogged_in_userフィルターに追加する。
app/controllers/users_controller.rb class UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update, :destroy] before_action :correct_user, only: [:edit, :update] . . . def destroy User.find(params[:id]).destroy flash[:success] = "User deleted" redirect_to users_url end private . . . end
サイトを正しく防衛するには、destroyアクションにもアクセス制御を行う必要があるので、
今回はbeforeフィルター(admin_user)を使ってdestroyアクションへのアクセスを制御する。
app/controllers/users_controller.rb class UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update, :destroy] before_action :correct_user, only: [:edit, :update] before_action :admin_user, only: :destroy . . . private . . . # 管理者かどうか確認 def admin_user redirect_to(root_url) unless current_user.admin? end end
ユーザー削除のテストを書く。
まずはユーザー用fixtureファイルを修正し、今いるサンプルユーザーの一人を管理者にする。
test/fixtures/users.yml michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> admin: true
管理者権限の制御をアクションレベルでテストする
test/controllers/users_controller_test.rb require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) @other_user = users(:archer) end . . . test "should redirect destroy when not logged in" do assert_no_difference 'User.count' do delete user_path(@user) end assert_redirected_to login_url end test "should redirect destroy when logged in as a non-admin" do log_in_as(@other_user) assert_no_difference 'User.count' do delete user_path(@user) end assert_redirected_to root_url end end
削除リンクとユーザー削除に対する統合テスト
test/integration/users_index_test.rb require 'test_helper' class UsersIndexTest < ActionDispatch::IntegrationTest def setup @admin = users(:michael) @non_admin = users(:archer) end test "index as admin including pagination and delete links" do log_in_as(@admin) get users_path assert_template 'users/index' assert_select 'div.pagination' first_page_of_users = User.paginate(page: 1) first_page_of_users.each do |user| assert_select 'a[href=?]', user_path(user), text: user.name unless user == @admin assert_select 'a[href=?]', user_path(user), text: 'delete' end end assert_difference 'User.count', -1 do delete user_path(@non_admin) end end test "index as non-admin" do log_in_as(@non_admin) get users_path assert_select 'a', text: 'delete', count: 0 end end
すべての変更をmasterブランチにマージ
$ git add -A $ git commit -m "Finish user edit, update, index, and destroy actions" $ git checkout master $ git merge updating-users $ git push
今日やったこと9
第10章 ユーザーの更新・表示・削除
10.1 ユーザーを更新する
ユーザーを編集するためのeditアクションを作成する。
def edit @user = User.find(params[:id]) end
editアクションに対応するeditビューを実装
app/views/users/edit.html.erb
WebブラウザはネイティブではPATCHリクエストを送信できないので、RailsはPOSTリクエストとinputフィールドを利用してPATCHリクエストを「偽造」している。
<input name="_method" type="hidden" value="patch" />
Railsは、form_for(@user)を使ってフォームを構成すると、@user.new_record?がtrueのときにはPOSTを、falseのときにはPATCHを使う。
$ rails console >> User.new.new_record? => true >> User.first.new_record? => false
ヘッダーに”Setting”リンクを追加する。
editビューへのパスとcurrent_userのヘルパーメソッドを使う。
<%= link_to "Settings", edit_user_path(current_user) %>
演習
・10.1.1.1
target="_blank"で新しいページを開くときには、セキュリティのためrel属性にnoopenerを設定する。
<a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a>
演習
・10.1.3.1
assert_selectを使ってalertクラスのdivタグを探しだし、「The form contains 4 errors.」というエラーメッセージが表示されているかテストする行を追加する。
assert_select "div.alert", "The form contains 4 errors."
Updateアクションを書くまえに編集の成功に対するテストを書く。
テストを先に書くことでユーザーが実際に使うイメージや追加するコードがわかりやすくなる。
class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end ・ ・ ・ # 編集の成功に対するテスト test "successful edit" do get edit_user_path(@user) assert_template 'user/edit' name = "Foo Bar" email = "foo@bar.com" patch user_path(@user), params: { user: { name: name, email: email, password: "", password_confirmation: "" } } assert_not flash.empty? assert_redirect_to @user @user.reload assert_equal name, @user.name assert_equal email, @user.email end end
ユーザー名やメールアドレスだけを編集したいときはパスワード欄を入力する必要はないが空のままだとバリテーションに引っかかってしまうので空のままでも更新できるようにvalidatesにallow_nil: trueというオプションを追加する。
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
updateアクションを作成しテストする。
def update @user = User.find(params[:id]) if @user.update_attributes(user_params) flash[:success] = "Profile updated" redirect_to @user else render 'edit' end end
10.2 認可
今のままでは誰でも (ログインしていないユーザーでも) ユーザー情報を編集できてしまうのでユーザーにログインを要求し、かつ自分以外のユーザー情報を変更できないようにする。
ログインしていないユーザーが保護されたページにアクセスしようとしたときはログインページに転送して、メッセージも表示するようにする。
許可されていないページに対してアクセスするログイン済みのユーザーがいたら 、ルートURLにリダイレクトさせるようにする。
転送させる仕組みを実装したいときは、Usersコントローラの中でbeforeフィルターを使う。beforeフィルターは、before_actionメソッドを使って何らかの処理が実行される直前に特定のメソッドを実行する仕組みである。
ユーザーにログインを要求するために、logged_in_userメソッドを定義してbefore_action :logged_in_userという形式で使用する。
デフォルトでは、beforeフィルターはコントローラ内のすべてのアクションに適用されるので、ここでは適切な:onlyオプション (ハッシュ) を渡すことで、:editと:updateアクションだけにこのフィルタが適用されるように制限をかけている。
app/controllers/users_controller.rb class UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] . . . private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end # beforeアクション # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? flash[:danger] = "Please log in." redirect_to login_url end end end
editアクションやupdateアクションをテストする前にログインしておく必要があるので、log_in_asヘルパーを使ってテストを修正する。
test/integration/users_edit_test.rb require 'test_helper' class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end test "unsuccessful edit" do log_in_as(@user) get edit_user_path(@user) . . . end test "successful edit" do log_in_as(@user) get edit_user_path(@user) . . . end end
beforeフィルターが機能しているかどうかのテストを追加する。
test/controllers/users_controller_test.rb require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end ・ ・ ・ # editアクションの保護に対するテスト test "should redirect edit when not logged in" do get edit_user_path(@user) assert_not flash.empty? assert_redirected_to login_url end # updateアクションの保護に対するテスト test "should redirect update when not logged in" do patch user_path(@user), params: { user: { name: @user.name, email: @user.email } } assert_not flash.empty? assert_redirected_to login_url end end
ユーザーが自分の情報だけを編集できるようにする必要があるためのテストを追加する。
ユーザーの情報が互いに編集できないことを確認するために、ユーザー用のfixtureファイルに2人目のユーザーを追加する。
test/fixtures/users.yml michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> # 2人目のユーザーを追加する archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %>
次に、 log_in_asメソッドを使って、editアクションとupdateアクションをテストする。
test/controllers/users_controller_test.rb require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) @other_user = users(:archer) #2人目のユーザー end . . . test "should redirect edit when logged in as wrong user" do log_in_as(@other_user) get edit_user_path(@user) assert flash.empty? #ユーザーが違う際にメッセージが出ているか assert_redirected_to root_url #ルートURLにリダイレクトしているか end test "should redirect update when logged in as wrong user" do log_in_as(@other_user) patch user_path(@user), params: { user: { name: @user.name, email: @user.email } } assert flash.empty? assert_redirected_to root_url end end
別のユーザーのプロフィールを編集しようとしたらリダイレクトするようにさせるためにcorrect_userというメソッドを作成し、beforeフィルターからこのメソッドを呼び出すようにする。
app/controllers/users_controller.rb class UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] #editとupdateの各アクションでcorrect_userを呼び出す before_action :correct_user, only: [:edit, :update] . . . def edit end def update if @user.update_attributes(user_params) flash[:success] = "Profile updated" redirect_to @user else render 'edit' end end . . . private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end # beforeアクション # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? flash[:danger] = "Please log in." redirect_to login_url end end # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless @user == current_user end end
今日やったこと8
Ruby on Rails チュートリアル(第4版) 第9章 発展的なログイン構造
(9.1 Remember me機能 〜 9章最後まで)
9.1 remenber機能
ユーザーのログイン状態をブラウザを閉じた後でも有効にする機能、ユーザーがログアウトしない限り、ログイン状態を維持できる。
トークン認証に記憶トークンを使うため、remenber_gigest属性をUserモデルに追加する。
9.1.2 ログイン状態の保持
ユーザーの暗号化済みIDと記憶トークンをブラウザの永続cookiesに保存して、永続セッションを作成するためcookiesメソッドをを使う。
記憶トークンと記憶ダイジェストをユーザごとに関連付けて、永続セッションが実現できる。
9.1.3 ユーザーを忘れる
ログアウトできるようにするため、user.forgetメソッドを定義し、update_attributeメソッドでremember_digestを取り消す。
9.2 Remember meチェックボックス
ログインフォームにチェックボックスを追加する。
他のフォームと同様にヘルパーメソッドで作成する。
セッションコントローラのcreateアクションにRemember meチェックボックスの痩身結果の処理を追加する。
Remember meチェックボックスのテスト。
今日やったこと7
Ruby on Rails チュートリアル(第4版) 第8章 基本的なログイン構造
(8.1 セッション 〜 8章最後まで)
セッション
SessionというRailsのメソッドを使って一時セッションを作成する。
UserリソースはバックエンドでUserモデルを介してデータベース上のデータにアクセスするが、Sessionリソースはcookiesを保存場所として使う。
Sessionコントローラを生成。
演習8.1.1.2で使ったパイプとgrepコマンド
パイプとは
コマンドとコマンドの間に「|」を使うことで複数のコマンドを組み合わせて使うことができる。
grepコマンド
指定したパターンにマッチする行を表示するコマンド。
ログインフォーム
SessionはActive Recordオブジェクトではない(Sessionモデルがない)のでユーザー登録フォームでform_forヘルパーを使う場合は引数にインスタンス変数ではなく、リソースの名前とそれに対応するURLを具体的に指定する必要がある。
ユーザーの検索と認証
ログインが送信されるたびに、パスワードとメールアドレスの組み合わせが有効かどうかを判断する。
認証にはUser.find_byメソッドとauthenticateメソッドを使う。
フラッシュメッセージを表示する
ユーザー登録の場合はActive Recordオブジェクトによってエラーメッセージを表示できたが、セッションではActive Recordのモデルを使っていないので自分でフラッシュメッセージを表示させる。
flashを使うとrenderメソッドでレンダリングしてもメッセージが残ったままになってしまうため、flash.nowを使う。
flash.nowのメッセージはその後リクエストが発生した時に消滅する。
ログイン
session[:user_id]を使ってユーザーIDを代入する。
sessionメソッドで作成された一時cookiesは、ブラウザを閉じた瞬間に有効期限が終了する。
ユーザーIDを一時セッションに保存できたら、そのユーザーIDを別のページで取り出すためにcurrent_userメソッドを定義する。
レイアウトリンクを修正・テストする。
ユーザー登録と同時にログインできるように修正。
ログアウト
ログアウトの処理ではセッションからユーザーIDを削除するためにdeleteメソッドを使う。
今日やったこと6
Ruby on Rails チュートリアル(第4版) 第7章 ユーザー登録
(7.3 ユーザー登録失敗 〜 7章最後まで)
・ユーザー登録を成功
新規ユーザーを実際にデータベースに保存できるようにし、ユーザー登録フォームを完成させる。
保存に成功すると、ユーザー情報は自動的にデータベースに登録され、次にブラウザの表示をリダイレクトして、登録されたユーザーのプロフィールを表示する。
Railsはデフォルトのアクションに対応するビューを表示しようとするが、Createアクションには対応するビューのテンプレートがない。テンプレートを作成することもできるが一般的には別のページ(今回は新しく作成されたユーザーのプロフィールページ)にリダイレクトするようにする。
・flash
登録完了後に表示させるページにメッセージを表示させるためにflash変数を使う。
flash[:success]といった:successキーを文字列として(本来はシンボルだが)利用することで、キーの内容によって異なったCSSクラスを適用させることができ、メッセージの種類によって動的に変更させることができる。
Bootstrap CSSはflashのクラス用に4つのスタイルがある(success、info、warning、danger)
・本番環境でのSSL(Secure Sockets Layer)
フォームで送信したデータはネットワーク上で補足できてしまうため、個人情報などの扱いには注意が必要で、このデータを保護するためにSLLを使用する。
SLLはローカルのサーバーからネットワークに流れる前に、大事な情報を暗号化する技術。
本番用のWebサイトでSSLを使えるようにするためには、ドメイン毎にSSL証明書を購入しセットアップする必要がある
Heroku上でサンプルアプリケーションを動かす場合は、Heroku上のSSL証明書に便乗する方法がある。(サブドメインでのみ有効)
HerokuのデフォルトではWEBrickというサーバを使っているが、本番環境として適切なサーバではないため、PumaというWebサーバに置き換える。