これまでのあらすじ
- 仮登録でメールアドレスを登録
- 登録したメールアドレスへ本登録画面に飛ぶURLを記載したメールを送信
- メールでURLへ遷移
- 登録に必要な情報の入力
- 登録完了
- ログイン
- ログアウト
までやっていきたいと思います。
メール送信
前の記事でユーザ仮登録のモーダルウィンドウ表示まで実装しました。
で、登録ボタンで以下のアクションを呼びます。
def create
@temp_user = TempUser.create_temp_user(temp_users_params)
respond_to do |format|
if @temp_user.save
UserMailer.with(temp_user: @temp_user, locale: params[:locale]).request_registration.deliver_later
format.js { @status = "success" }
else
format.js { @status = "fail" }
end
end
end
TempUserクラスの#create_temp_userメソッドで初期化します。
# 仮ユーザの作成
def create_temp_user(params)
# temp_usersにmail_addressで検索 初期化する
temp_user = find_or_initialize_by(mail_address: params[:mail_address])
temp_user.last_name = params[:last_name]
temp_user.first_name = params[:first_name]
temp_user.token = create_token
temp_user.expired_at = DateTime.now + 1
return temp_user
end
find_or_initialize_byでメールアドレスを検索します。
もし、仮登録済みで本登録していないメールアドレスの場合、後勝ちにして最後に入力した情報でUPDATEします。
もし、temp_userにない場合は、入力情報でINSERTします。
find_or_initialize_byを使用し、saveをするタイミングでINSERTかUPDATEか判定、UPSERTが実現できます。
expired_at(有効期限)は、とりあえず1日にしていますが、設定を外出ししたいですね。
saveメソッドの戻り値でSQLの成功・失敗を判定して処理を判定しています。
成功時は、登録情報を元にUserMailerでメールを作成しています。
class UserMailer < ApplicationMailer
default from: "hogehoge@gmail.com"
def request_registration
@temp_user = params[:temp_user]
@locale = params[:locale]
mail(to: @temp_user.mail_address, subject: I18n.t("mailers.user_mailer.request_registration.subject"))
end
end
ApplicationMailerを継承したUserMailerです。
from:は送信元のメールアドレスを設定します。
request_registrationがメール送信の本体です。
paramsで引数を受け取ってメールを生成します。
メール本体は、viewsの下に作ります。
<%= stylesheet_link_tag "mailers/request_registration.css", media: "all" %>
<p class="message"><%= t('mailers.user_mailer.request_registration.dear', last_name: @temp_user.last_name, first_name: @temp_user.first_name) %></p>
<pre class="message"><%= t('mailers.user_mailer.request_registration.message_text', expired_at: l(@temp_user.expired_at, format: :default)) %></pre>
<div class="btn">
<%= link_to t('mailers.user_mailer.request_registration.button'), {controller: 'account', action: 'regist', locale: @locale, token: @temp_user.token } %>
</div>
メソッド名のhtml.erb(HTMLメール)またはtext.erb(テキストメール)を雛形として作ります。
上記は、HTMLメールの雛形です。link_toでaccount_controller.rbのregistメソッドを指定しています。temp_user登録の際、生成したtokenをGETリクエストをパラメタとしてURLに付与し、link_toで生成しています。
メールを受信する
開発中に動作確認したいですが、実際に送ると、誤送信する恐れがあるので、以下のgemを入れます。
# letter_opener_web
gem 'letter_opener_web', '~> 1.0'
このgemはメールを送信・受信してくれます。
config/environments/development.rbに設定を追加します。
config.action_mailer.delivery_method = :letter_opener_web
また、routes.rbに以下のパスを設定し、送信メールを確認できるようにします。
# letter_opener_web
mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
これで(http://localhost:3000/letter_opener) でアプリが送信したメールをブラウザで確認できます。
登録
メールに記載されたURLを押下すると、登録に必要な情報を入力する画面へ遷移できます。
パスワードについて
ライブラリを使わないでログイン機能を実装するために以下のgemを使います。
# Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7'
Userモデルにhas_secure_passwordを追加します。
class User < ApplicationRecord
include ActiveModel::Validations
has_secure_password validations: true
validates :last_name, presence: true
validates :first_name, presence: true
validates :last_name_roman, upper_case_format: true, unless: Proc.new { |p| p.last_name_roman.blank? }
validates :first_name_roman, upper_case_format: true, unless: Proc.new { |p| p.first_name_roman.blank? }
validates :sex, inclusion: { in: [0, 1] }
validates :birthed_on, presence: true
validates :mail_address, uniqueness: true
validates :password, password_format: true
validate :already_used_mail_address
# already used mail address
def already_used_mail_address
unless User.find_by(mail_address: mail_address).nil?
errors.add(:mail_address, I18n.t("validate.already_use"))
end
end
# create remember token
def self.create_remember_token
SecureRandom.urlsafe_base64
end
# encrypt
def self.encrypt(token)
Digest::SHA256.hexdigest(token.to_s)
end
end
passwordとpassword_confirmation属性、さらにauthenticateメソッドが使用できるようになります。
さらにDB内ではpassword_digestというカラムで暗号化されたパスワードは保存されます。
(password_digestをmigrationで対象のテーブルに追加します。)
アプリケーションログには、入力値は当然出力されず
DBには、暗号化されたパスワード文字列が登録されます。
登録ボタンでcreateメソッドが呼び出され、登録成功したら完了画面、失敗したら再度登録画面をレンダリングします。
def create
@user = User.new(users_params)
if @user.save
render action: :complete
else
render action: :regist
end
end
ログイン・ログアウト
application_controller.rbに以下を定義します。
class ApplicationController < ActionController::Base
# filter
# actionの直前に実行されるfilter
before_action :set_locale
before_action :current_user
before_action :require_sign_in!
# helper methodとして使用できる
helper_method :signed_in?
def set_locale
I18n.locale = locale
end
def locale
@locale ||= params[:locale] ||= I18n.default_locale
end
def default_url_options(options = {})
options.merge(locale: locale)
end
def current_user
remember_token = User.encrypt(cookies[:remember_token])
@current_user ||= User.find_by(remember_token: remember_token)
end
def sign_in(user)
remember_token = User.create_remember_token
# cookieにremember_tokenをsetする
cookies.permanent[:remember_token] = remember_token
# remember_tokenを更新する
user.update_column(:remember_token, User.encrypt(remember_token))
@current_user = user
end
def sign_out
@current_user = nil
# cookieのremember_tokenを削除する
cookies.delete(:remember_token)
redirect_to login_path
end
def signed_in?
@current_user.present?
end
private
def require_sign_in!
redirect_to login_path unless signed_in?
end
end
filter処理として
current_userメソッド
ログイン時にCookieとDBに登録したトークン情報を突き合わせて、ユーザ情報を取得する
require_sign_in!メソッド
ユーザ情報が取得できない場合(=ログインしていない場合)ログイン画面へリダイレクトする
これをapplication_controller.rbに定義することによりapplication_controllerを継承する全てのcontrollerにこのfilter処理が走ります。
これにより画面上でログイン状態・非ログイン状態を判断するわけです。
ログイン処理はsession_controller.rbに定義しています。
class SessionsController < ApplicationController
# filter
# actionの直前に実行されるfilterをskipする
skip_before_action :require_sign_in!, only: [:new, :create]
# actionの直前に実行されるfilter
before_action :set_user, only: [:create]
# GET /login
def new
redirect_to root_path
end
# PUT /login
def create
if @user.authenticate(@session.password)
sign_in(@user)
else
@session.sign_in_failure
end
render "top/index"
end
# DELETE /logout
def destroy
sign_out
redirect_to login_path
end
private
def set_user
@session = Session.new(session_params)
if !@session.valid?
render "top/index" and return
end
@user = User.find_by!(mail_address: @session.mail_address)
rescue
@session.sign_in_failure
render "top/index"
end
def session_params
params.require(:session).permit(:mail_address, :password)
end
end
Sessionモデルはmail_addressとpasswordのログイン認証に必要な情報をもっているクラスです。
skip_before_actionは、actionの直前に実行されるfilterをskipするための宣言です。
当然ログイン画面表示とログイン処理にログイン認証のfilterが入っているとログインできないため、skipしています。
createメソッドの前のみset_userのfilterが適用されます。
set_userメソッドは画面から入力されたフォーム情報を元にmail_addressでユーザ情報を取得します。
取得できない場合、例外を発生させます。例外発生時はログイン画面をレンダリングします。
createメソッドは、set_userメソッドで取得したユーザ情報とパスワードを確認します。
確認できた場合、ログイン処理(application_controller.rbに定義)をします。
ログイン処理は、ログイン認証していることを示す、remember_tokenを画面に保持します。
remember_tokenはUserクラスで定義したメソッドでランダム文字列で生成されます。
それをCookieに保存し、その後DB(userテーブル)UPDATEします。
前述のログイン確認のfilterはこのremember_tokenを元にログインしているか、していないかを判断するというわけです。
終わりに
- 勉強会の資料なので、急いで作ったので、もうちょっと修正します。
- 参考リンクとかも記載せねば…
参考
工事中…