Rails: アカウント登録
目的
Railsアプリのアカウント登録処理を実装する。
日本語化などは今回の対象外にする。
環境
Ruby 2.7.2
Rails 6.1.3
リポジトリ
https://github.com/usutani/try_account
画面遷移
Breadboarding で描いてみる。
基本手順
1. システムは、アカウント登録用の入力フォームを表示する。
2. ユーザーは、メールアドレスを入力し、「利用規約に同意する」をチェックして、「登録する」ボタンをクリックする。
3. システムは、入力された情報に不備がないかを確認し、データベースに登録情報を保存する。
4. システムは、確認メール送信済を知らせるフォームを表示する。メール再送信の機能は提供しない。アカウント登録からやり直してもらう。
5. システムは、入力されたメールアドレスに確認メールを送信する。メール本文に含めるURLの有効期間は1時間とする。
6. ユーザーは、確認メールのURLをクリックする。
7. システムは、URLに不備がないかを確認し、アカウント作成用の入力フォームを表示する。アカウントのメールアドレスは登録情報のメールアドレスを設定する。
8. ユーザーは、パスワードを入力し、「登録する」ボタンをクリックする。
9. システムは、入力された情報に不備がないかを確認し、データベースにアカウントを保存する。メールアドレスはフォームから設定できない。
10. システムは、登録したアカウントのフォームを表示する。
代替手順
3. 不備がある場合:システムは、エラーを入力フォームに表示する。
- メールアドレスを入力してください。
- メールアドレスは既に使用されています。使い勝手を良くするために、この時点でアカウント側を検索してエラーを表示する。
- 利用規約に同意するにチェックしてください。
7. 不備がある場合:システムは、エラー(422 Unprocessable Entity)を返す。
- URLの有効期限切れ。
9. 不備がある場合:システムは、エラーを入力フォームに表示する。
- メールアドレスを入力してください。(入力フォームからは設定させない)
- メールアドレスは既に使用されています。
- パスワードを入力してください。
- パスワードが短すぎます。(8文字以上)
- パスワードが長すぎます。(30文字以下)
- パスワードは半角英数とハイフン(-)のみお使いください。
ルーティング
アカウント登録用の入力フォームを表示する:account/registrations/new
確認メール送信済を知らせるフォームを表示する:account/registrations/:id
アカウント作成用の入力フォームを表示する:accounts/new?expiring_sid=FooBar...
# config/routes.rb
Rails.application.routes.draw do
namespace :account do
resources :registrations, only: %i[show new create]
end
resources :accounts, only: %i[show new create]
end
登録(registrations)のルーティング
リソースの名前空間を単数形のaccountにする。
後述する足場作成でモデルとコントローラーについても名前空間を単数形にした。
この時に生成されるリソースのルーティングに合わせている。
モデル
- Account
- Account::Registration
# app/models/account.rb
class Account < ApplicationRecord
has_secure_password
validates :email, presence: true, uniqueness: true
validates :password, length: { in: 8..30 },
format: { with: /\A[a-z0-9]+\z/i,
message: 'は半角英数とハイフン(-)のみお使いください。' }
end
# app/models/account/registration.rb
class Account::Registration < ApplicationRecord
validates :email, presence: true
validate :email_of_account_is_unique
validates :terms_of_service, acceptance: true
private
def email_of_account_is_unique
errors.add(:email, 'は既に使用されています。') if Account.find_by(email: email)
end
end
アカウントと登録情報のメールアドレスは、必須入力にする。
RFC違反のメールアドレスについても扱いたいため、フォーマットの検証は行わない。
確認メールが届いたことを妥当性が通ったことにする。
登録情報はイベントとして記録する。
従ってメールアドレスでユニークにしない。
Active Record バリデーション - Railsガイド > 2.1 acceptance
フォームが送信されたときにユーザーインターフェイス上のチェックボックスがオンになっているかどうかを検証します。このチェックはnilでない場合にのみ実行されます。
アプリの準備
認証の習作- has_secure_password|usutani|note
has_secure_passwordを使うためにbcryptを有効にする。
rails new try_account \
--skip-action-mailbox \
--skip-action-text \
--skip-active-storage \
--skip-action-cable \
--skip-javascript \
--skip-turbolinks \
--skip-jbuilder \
--skip-test
cd try_account && tmux
bin/rails console
require 'rails/generators'
class Foo < Rails::Generators::Base; end
Foo.new.uncomment_lines "Gemfile", %(gem 'bcrypt')
exit
アカウントの足場作成
モデルとコントローラーの名前空間を単数形にする。
コントローラーの親ディレクトリが単数形のaccountになる。
尚、コントローラーのファイル名は複数形で生成される。account/registrations_controller.rb
コントローラー単体を生成する場合は複数形の名称を指定する必要がある。
bin/rails g scaffold Account email:uniq password:digest
bin/rails g scaffold account/registration email
# Overwrite app/models/account.rb? (enter "h" for help) [Ynaqdhm] n
bin/rails db:migrate
bin/rails s
open http://localhost:3000/account/registrations/new
# config/routes.rb
# 既出
# app/models/account/registration.rb
# 既出
利用規約をコントローラーとビューに追加する。
# app/controllers/account/registrations_controller.rb
def account_registration_params
params.require(:account_registration).permit(:email, :terms_of_service)
end
# app/views/account/registrations/_form.html.erb
<div class="field">
<%= form.label :terms_of_service %>
<%= form.check_box :terms_of_service %>
</div>
open http://localhost:3000/account/registrations/new
署名ID(signed_id)を用いたURLの生成と確認
bin/rails console --sandbox
@account_registration = Account::Registration.create!(email: 'foo@example.com')
expiring_sid = @account_registration.signed_id(expires_in: 1.hour, purpose: :registration) # => "FooBar...
app.new_account_url(expiring_sid: expiring_sid) # => "http://www.example.com/accounts/new?expiring_sid=FooBar...
Account::Registration.find_signed(expiring_sid, purpose: :registration) # => Account::Registration.first
Account::Registration.find_signed('INVALID', purpose: :registration) # => nil
exit
無効であればnilを返す。
メーラー作成前なので、一旦、コントローラーのメールを送信する箇所でログにURLを出力しておく。
# app/controllers/account/registrations_controller.rb
def create
@account_registration = Account::Registration.new(account_registration_params)
if @account_registration.save
expiring_sid = @account_registration.signed_id(expires_in: 1.hour, purpose: :registration)
logger.info new_account_url(expiring_sid: expiring_sid)
redirect_to @account_registration, notice: 'Registration was successfully created.'
else
render :new, status: :unprocessable_entity
end
end
# app/views/account/registrations/show.html.erb
-
-<%= link_to 'Edit', edit_account_registration_path(@account_registration) %> |
-<%= link_to 'Back', account_registrations_path %>
<%= link_to 'アカウント登録(やり直し)', new_account_registration_path %>
ログ出力を確認する。
open http://localhost:3000/account/registrations/new
Started POST "/account/registrations" for ::1 at 2021-01-01 00:00:00 +0900
Processing by Account::RegistrationsController#create as HTML
Parameters: {"authenticity_token"=>"[FILTERED]", "account_registration"=>{"email"=>"foo@example.com", "terms_of_service"=>"1"}, "commit"=>"Create Registration"}
...
↳ app/controllers/account/registrations_controller.rb:26:in `create'
http://localhost:3000/accounts/new?expiring_sid=FooBar...
確認メールの送信
メーラークラスを生成する。
今回、HTMLメールは送らない。
bin/rails g mailer account/registration confirm
rm app/views/account/registration_mailer/confirm.html.erb
2.7 Action MailerのビューでURLを生成する
# config/application.rb
# config.action_mailer.default_url_options = { host: 'example.com' }
config.action_mailer.default_url_options = { host: 'localhost:3000' }
# app/mailers/account/registration_mailer.rb
class Account::RegistrationMailer < ApplicationMailer
def confirm
@account_registration = params[:account_registration]
mail(to: @account_registration.email, subject: 'Please confirm your registration')
end
end
コントローラーのコードをメーラーのビューに移す。
# app/views/account/registration_mailer/confirm.text.erb
<% expiring_sid = @account_registration.signed_id(expires_in: 1.hour, purpose: :registration) %>
<%= new_account_url(expiring_sid: expiring_sid) %>
コントローラーでメールを送信する。
# app/controllers/account/registrations_controller.rb
def create
@account_registration = Account::Registration.new(account_registration_params)
if @account_registration.save
Account::RegistrationMailer.with(account_registration: @account_registration).confirm.deliver_later
redirect_to @account_registration, notice: 'Registration was successfully created.'
else
render :new, status: :unprocessable_entity
end
end
ログ出力を確認する。
open http://localhost:3000/account/registrations/new
Subject: Please confirm your registration
Mime-Version: 1.0
Content-Type: multipart/alternative;
...
http://localhost:3000/accounts/new?expiring_sid=FooBaz...
出力したURLをウェブブラウザで開く。
open http://localhost:3000/accounts/new?expiring_sid=FooBaz...
アカウントのバリデーション
# app/models/account.rb
# 既出
アカウントの作成
署名ID(expiring_sid)から登録情報を復元する。
署名IDが無効ならエラー(422 Unprocessable Entity)を返す。
メールアドレスは登録情報のメールアドレスを設定する。
メールアドレスは入力フォームから設定させない。
# app/controllers/accounts_controller.rb
class AccountsController < ApplicationController
before_action :set_account, only: [:show, :edit, :update, :destroy]
before_action :set_expiring_sid_and_account_registration, only: %i[new create]
#...
def new
@account = Account.new
@account.email = @account_registration.email
end
#...
def create
@account = Account.new(account_params)
@account.email = @account_registration.email
if @account.save
redirect_to @account, notice: 'Account was successfully created.'
else
render :new, status: :unprocessable_entity
end
end
#...
private
# Use callbacks to share common setup or constraints between actions.
def set_account
@account = Account.find(params[:id])
end
def set_expiring_sid_and_account_registration
@expiring_sid = params[:expiring_sid] || params[:account][:expiring_sid]
@account_registration = Account::Registration.find_signed(@expiring_sid, purpose: :registration)
head :unprocessable_entity if @account_registration.nil?
end
# Only allow a list of trusted parameters through.
def account_params
- params.require(:account).permit(:email, :password, :password_confirmation)
params.require(:account).permit(:password, :password_confirmation)
end
メールアドレスを表示のみに変更する。
バリデーションエラーの表示用に署名IDをhidden_fieldで渡しておく。これはセッションで渡すのも良いかもしれない。
# app/views/accounts/_form.html.erb
- <div class="field">
- <%= form.label :email %>
- <%= form.text_field :email %>
- </div>
<p>
<strong>Email:</strong>
<%= @account.email %>
</p>
<%= form.hidden_field :expiring_sid, value: @expiring_sid %>
アカウント編集と一覧へのリンクを削除する。
# app/views/accounts/show.html.erb
-<%= link_to 'Edit', edit_account_path(@account) %> |
-<%= link_to 'Back', accounts_path %>
アカウントの作成
open http://localhost:3000/accounts/new?expiring_sid=FooBaz...
条件を満たせばアカウントの作成に成功する。
アカウントの登録
使用済みのメールアドレスは登録できない。
open http://localhost:3000/account/registrations/new
続き(Rails- アカウント凍結と一覧表示)はこちらです。
付録
Rails: 覚書: has_secure_passwordのバリデーション
以上です。
この記事が気に入ったらサポートをしてみませんか?