Rails: アカウント登録

目的
Railsアプリのアカウント登録処理を実装する。
日本語化などは今回の対象外にする。

環境
Ruby 2.7.2
Rails 6.1.3

リポジトリ
https://github.com/usutani/try_account

画面遷移
Breadboarding で描いてみる。

画像1

基本手順
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

画像2

署名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
# 既出

画像3

アカウントの作成

署名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...

画像4

条件を満たせばアカウントの作成に成功する。

画像5

アカウントの登録
使用済みのメールアドレスは登録できない。

open http://localhost:3000/account/registrations/new

画像6

続き(Rails- アカウント凍結と一覧表示)はこちらです。

付録
Rails: 覚書: has_secure_passwordのバリデーション


以上です。

この記事が気に入ったらサポートをしてみませんか?