メール機能を使ってsignupを改善する
ブランチを変える
git checkout -b account-activation
AccountActivationsのフォルダを作成
rails generate controller AccountActivations
新しくマイグレーションファイルを作成 またカラム名とデータ型を指定する
rails generate migration add_activation_to_users activation_digest:string activated:boolean activated_at:datetime
editアクションへの名前付きルートが必要になるので
名前付きルートを扱えるようにルーティングにアカウント有効化用のresources行を追加
編集だけにする
Rails.application.routes.draw do
root 'static_pages#home'
.
.
.
resources :account_activations, only: [:edit]
#名前付きルートを扱えるようにするため
#ルーティングにアカウント有効化用のresources行を追加
# 編集だけ
end
指定したために作成したファイルのコードに名前とデータ型が書かれてある。
booleanではtrue,false,nilの3つがある。のでnilが出ないようにデフォルトで指定しておく。
class AddActivationToUsers < ActiveRecord::Migration[6.0]
def change
add_column :users, :activation_digest, :string
add_column :users, :activated, :boolean default: false
add_column :users, :activated_at, :datetime
end
end
migrateはmigrationファイルの内容をDBに反映する行為
rails db:migrate
Userモデルにアカウント有効化のコードを追加する
class User < ApplicationRecord
attr_accessor :remember_token, :activation_token
before_save :downcase_mail
before_create :create_activation_digest
.
.
.
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
開発環境のサンプルユーザーを最初から有効にしておく
User.create!(name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar",
admin: true,
activated: true,
activated_at: Time.zone.now)
# 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,
activated: true,
activated_at: Time.zone.now)
end
テスト環境のユーザーも同じように書く。
データベースを初期化して、サンプルデータを再度生成し直し、変更を反映
$ rails db:migrate:reset
$ rails db:seed
送信メールのテンプレート
メイラーは、モデルやコントローラと同様にrails generateで生成
アカウント有効化メールの送信に必要なコードを追加のため
account_activation,password_resetはアクション名
UserMailerはコントローラ名という感じ
rails generate mailer UserMailer account_activation password_reset
生成されたApplicationメイラーのデフォルト値を更新した。
class ApplicationMailer < ActionMailer::Base
default from: "noreply@example.com"
# アドレスのデフォルト値を更新
layout 'mailer'
# layout コントローラーごとに各ページ共通の
#ビューファイルを指定できるメソッド
# mailerのページを指定する
end
Userメイラーを生成し、アカウント有効化リンクをメール送信する様にする
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
# 内容はわからん
送信メールのプレビュー
テンプレートの実際の表示を簡単に確認するためメールプレビューを使う。
Railsでは、特殊なURLにアクセスするとメールのメッセージをその場でプレビューすることができる。
これを利用するには、アプリケーションのdevelopment環境の設定に手を加える必要がある。
development環境のメール設定
Rails.application.configure do
.
.
.
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
# '/account_activation/:token/edit?email=foobar@bar.com'
host = '開発環境のurl'
# example.comにはsample_appは無いから
# ここをコピペすると失敗します。
# 自分の環境のホストに変えてください。
# クラウドIDEの場合は以下をお使いください
config.action_mailer.default_url_options = { host: host, protocol: 'https' }
.
.
.
end
アカウント有効化のプレビューメソッド
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview
# Preview this email at /rails/mailers/user_mailer/account_activation
def account_activation
user = User.first
user.activation_token = User.new_token
# activation_tokenは仮の属性でしかない
# データベースのユーザーはこの値を実際には持っていません
UserMailer.account_activation(user)
# UserMailerクラスのメソッド
# 引数はメールオブジェクト(メールのプレビュー)
# 開発用データベースの最初のユーザーに定義して
#UserMailer.account_activationの引数として渡す
end
# Preview this email at http://localhost:3000/rails/mailers/user_mailer/password_reset
def password_reset
UserMailer.password_reset
end
end
# だいたいわからん
現在のメールの実装をテストする
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
users.rb
def authenticated?(attribute, token)
digest = send("#{attribute}_digest")
# attributeがremember,activation,resetだったりする
return false if digest.nil?
# remember_digestが空ならばfalseを返す
# 先にreturnをすることでエラーを起こさないようにする
# remember_tokenが空(nil)だから
BCrypt::Password.new(remember_digest).is_password?(remember_token)
# データベースのremember_digestとトークンのremember_token
#を照らし合わせる
end
テストを行うとエラーが起こる
Error:
SessionsHelperTest#test_current_user_returns_right_user_when_session_is_nil:
ArgumentError: wrong number of arguments (given 1, expected 2)
expected 引数は2つを期待しているが given 引数は1個しかない
app/models/user.rb:49:in `authenticated?'
app/helpers/sessions_helper.rb:20:in `current_user'
test/helpers/sessions_helper_test.rb:15:in `block in <class:SessionsHelperTest>'
user_test.rb
test "authenticated? should return false for a user with nil digest" do
# 認証する? ダイジェストが空(nil)のユーザーにはfalseを返す必要があります
assert_not @user.authenticated?(:remember, '')
# 空のデータベースは認証されないよね?
end
Failure:
SessionsHelperTest#test_current_user_returns_right_user_when_session_is_nil [/home/ubuntu/environment/sample_app/test/helpers/sessions_helper_test.rb:15]:
--- expected
+++ actual
@@ -1 +1 @@
-#<User id: 762146111, name: "Michael Example", email: "michael@example.com", created_at: "2021-07-31 15:33:09", updated_at: "2021-07-31 15:33:13", password_digest: [FILTERED], remember_digest: "$2a$04$iDPLGzGFNJBBoRUPTNCKL.mdoxo7NQTKSPy3G8YKU/I...", admin: true, activation_digest: nil, activated: true, activated_at: "2021-07-31 15:33:09">
+nil
-#<User....の情報が来るべきだが +nil その情報が来ない というエラーらしい
editアクションを書く準備ができた
paramsハッシュで渡されたメールアドレスに対応するユーザーを認証
既に有効になっているユーザーを誤って再度有効化しないために必要
このコードがなければ、攻撃者がユーザーの有効化リンクを後から盗みだしてクリックするだけで、本当のユーザーとしてログインできてしまいます。
class AccountActivationsController < ApplicationController
def edit
ef 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
ここで使用していたインスタンスが消えてしまった。
また1から頑張りたい。
困ったこと
rails generate migration add_activation_to_usersしか入力せずにmigrationファイルを作成してしまった。
rails generate migration add_activation_to_users
このファイルを削除するために入力した。
rails destroy migration add_activation_to_users
見事に成功。
また新しくファイルを作成した。
rails generate migration add_activation_to_users activation_digest:string activated:boolean activated_at:datetime
これも成功した。
Error:
UsersSignupTest#test_valid_signup_information:
AbstractController::DoubleRenderError: Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action. Also note that neither redirect nor render terminate execution of the action, so if you want to exit an action after redirecting, you need to do something like "redirect_to(...) and return".
app/controllers/users_controller.rb:54:in `create'
test/integration/users_signup_test.rb:18:in `block (2 levels) in <class:UsersSignupTest>'
test/integration/users_signup_test.rb:17:in `block in <class:UsersSignupTest>'</font>
rails test test/integration/users_signup_test.rb:15
なんと言っているわからないが、
user_controllerの54行目を確認してみたら
log_in @user
flash[:success] = "Welcome to the Sample App!"
redirect_to @user
を削除していなかった。 動画通りになった。
鈍臭い...。
また困りごとが解決したら書きたいと思う。
努力したい。