【Rails/devise】会員登録時に関連付けられたモデルを同時に作成する
今回は複数のユーザーが同じ企業アカウントのデータへのアクセスを共有する必要があるサービスがあると想定してUser(企業の社員)を登録時に紐づくAccount(企業)を同時に作成する登録フォームを作成します。
Rails7.1 + ESbuild + Tailwind CSSでの環境構築、 RSpecの導入が完了している前提で進めていきます。
環境構築の記事はこちら、RSpecの導入の記事はこちらで紹介しています。
Accountモデルの作成
accountsテーブルの作成
string
型のname
カラムを持つAccount
モデルを作成します。
rails g model Account name:string
name
カラムは必須にするため、作成されたマイグレーションファイルを以下のように編集します。
class CreateAccounts < ActiveRecord::Migration[7.1]
def change
create_table :accounts do |t|
t.string :name, null: false
t.timestamps
end
end
end
編集が完了したらmigrate
します。
rails db:migrate
バリデーションの追加
Account
モデルにバリデーション定義も追加します。
class Account < ApplicationRecord
validates :name, presence: true
end
deviseを導入
gemを追加
Gemfile
にdevise
を追加します。
gem "devise"
bundle install
インストール
gem
のインストール後、deviseのインストールを行います。
rails g devise:install
config/environments/development.rb
, config/environments/test.rb
にメーラーのデフォルトURLオプションを指定します。
Rails.application.configure do
# 省略
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
end
Userモデルの作成
devise
を使ってAccount
に紐付けるUser
モデルを作成します。
追加のカラムとしてfrist_name
, last_name
, アカウント内での役割であるrole
, サービス全体の管理者を判別するadmin
カラムを指定しています。
rails g devise User account:references first_name:string last_name:string role:integer admin:boolean
migrationファイルに以下の追記をします。
- メール認証を行うため、
Confirmable
のコメントを外す account
との関連付けにnot null
制約、外部キー制約を追加admin
カラムにデフォルト値を追加
# frozen_string_literal: true
class DeviseCreateUsers < ActiveRecord::Migration[7.1]
def change
create_table :users do |t|
## Database authenticatable
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
## Recoverable
t.string :reset_password_token
t.datetime :reset_password_sent_at
## Rememberable
t.datetime :remember_created_at
## Trackable
# t.integer :sign_in_count, default: 0, null: false
# t.datetime :current_sign_in_at
# t.datetime :last_sign_in_at
# t.string :current_sign_in_ip
# t.string :last_sign_in_ip
## Confirmable
# ↓コメントアウトを外す↓
t.string :confirmation_token
t.datetime :confirmed_at
t.datetime :confirmation_sent_at
t.string :unconfirmed_email # Only if using reconfirmable
# ↑コメントアウトを外す↑
## Lockable
# t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
# t.string :unlock_token # Only if unlock strategy is :email or :both
# t.datetime :locked_at
t.integer :role, null: false, default: 0 # デフォルト値を追加
t.references :account, null: false, foreign_key: true # not null制約、外部キー制約を追加
t.string :first_name
t.string :last_name
t.boolean :admin, null: false, default: false # デフォルト値を追加
t.timestamps null: false
end
add_index :users, :email, unique: true
add_index :users, :reset_password_token, unique: true
# add_index :users, :confirmation_token, unique: true
# add_index :users, :unlock_token, unique: true
end
end
編集が完了したらmigrate
します。
rails db:migrate
user.rb
にconfirmable
の記述を追加し、enum
でrole
を定義します。
今回は一般ユーザーであるgeneralとオーナーユーザーであるownerを定義しています。
後に、オーナーのみがアカウントにユーザーを招待できる機能を実装予定です。
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable, :confirmable
enum role: { general: 0, owner: 1 }
end
AccountモデルとUserモデルの関連付けの設定を行います。
class Account < ApplicationRecord
validates :name, presence: true
has_many :users, dependent: :destroy
end
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable, :confirmable
belongs_to :account
accepts_nested_attributes_for :account
enum role: { general: 0, owner: 1 }
end
今回は1つのフォームでAccount
とUser
を同時に作成するため、user.rb
でaccepts_nested_attributes_for :account
を定義していきます。
これにより、User
作成時にAccount
の属性をaccount_attributes
として渡すことができます。
ログイン後のダッシュボードを作成
Dashboardコントローラー作成
ログインした後に表示される仮のダッシュボードページを作成しておきます。
rails g controller Dashboard show
ログイン時未ログイン時のルートパスを定義
routes.rb
を以下のように編集します。
未ログイン時はログイン画面、ログイン時はダッシュボード画面をルート画面としています。
Rails.application.routes.draw do
devise_for :users
authenticate :user do
root to: "dashboard#show", as: :user_root
end
devise_scope :user do
root to: "devise/sessions#new"
end
end
registrationsをカスタマイズ
User
作成時にAccount
を作成できるようにデフォルトのregistrations
コントローラーをカスタマイズしていきます。
devise
をインストールしただけではregistrations
コントローラーはプロジェクト内に作成されないので、追加で作成します。
rails g devise:controllers users -c=registrations
registrations_controller.rb
を以下の内容で編集します。
# frozen_string_literal: true
class Users::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create]
# before_action :configure_account_update_params, only: [:update]
# GET /resource/sign_up
def new
build_resource
resource.build_account # userに紐づくaccountを初期化
yield resource if block_given?
respond_with resource
end
# POST /resource
# def create
# super
# end
# GET /resource/edit
# def edit
# super
# end
# PUT /resource
# def update
# super
# end
# DELETE /resource
# def destroy
# super
# end
# GET /resource/cancel
# Forces the session data which is usually expired after sign
# in to be expired now. This is useful if the user wants to
# cancel oauth signing in/up in the middle of the process,
# removing all OAuth session data.
# def cancel
# super
# end
protected
def build_resource(hash = {})
self.resource = resource_class.new_with_session(hash, session)
self.resource.role = :owner
end
# If you have extra params to permit, append them to the sanitizer.
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up, keys: [
account_attributes: [:name]
])
end
# If you have extra params to permit, append them to the sanitizer.
# def configure_account_update_params
# devise_parameter_sanitizer.permit(:account_update, keys: [:attribute])
# end
# The path used after sign up.
# def after_sign_up_path_for(resource)
# super(resource)
# end
# The path used after sign up for inactive accounts.
# def after_inactive_sign_up_path_for(resource)
# super(resource)
# end
end
カスタマイズ内容について軽く説明します。
デフォルトのregistrations_controller
の内容は公式のGitHubから確認できます。
newアクション
デフォルトのnew
アクションにresource.build_account
の記述を追加して、user
に紐づくaccount
を初期化します。
def new
build_resource
resource.build_account
yield resource if block_given?
respond_with resource
end
build_resourceメソッド
build_resource
メソッドにself.resource.role = :owner
を追加して登録フォームからの作成はデフォルトでオーナーの役割を持つユーザーになるように設定します。
def build_resource(hash = {})
self.resource = resource_class.new_with_session(hash, session)
self.resource.role = :owner
end
configure_sign_up_paramsメソッド
configure_sign_up_params
メソッドを編集して、パラメータとしてでaccount
の属性であるname
を受け取れるように設定しています。
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up, keys: [
account_attributes: [:name]
])
end
カスタマイズした内容を反映できるようroutes.rb
の内容を編集します。
Rails.application.routes.draw do
devise_for :users,
controllers: {
registrations: "users/registrations"
}
# 省略
end
viewsをカスタマイズ
登録画面は現状だとUser
作成用になっているのでAccount
のname
属性の欄追加と、デザインのカスタマイズができるようにdevise
関連のview
ファイルを生成します。
rails g devise:views
tailwindcss-formsをインストール
フォームのデザインを整えるためにTailwind CSS
のプラグインであるtailwindcss-formsをインストールします。
yarn add @tailwindcss/forms
tailwind.config.js
でプラグインの読み込みを行います。
module.exports = {
// 省略
plugins: [
require('@tailwindcss/forms'),
]
}
登録画面のカスタマイズ
以下の内容で登録画面を編集します。
<div class="flex flex-col justify-center mt-12">
<h2 class="text-center text-3xl font-extrabold text-gray-900">
Sign up
</h2>
<div class="bg-white mt-8 mx-auto w-full max-w-lg p-8 shadow rounded-lg">
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |form| %>
<%= form.fields_for :account do |account_fields| %>
<%= render "devise/shared/error_messages", resource: resource %>
<div class="mb-4">
<%= account_fields.label :name, class: "block text-gray-700 text-sm font-bold mb-2" %>
<%= account_fields.text_field :name, autofocus: true, class: "shadow border-gray-200 rounded w-full" %>
</div>
<div class="mb-4">
<%= form.label :email, class: "block text-gray-700 text-sm font-bold mb-2" %>
<%= form.email_field :email, autocomplete: "email", class: "shadow border-gray-200 rounded w-full" %>
</div>
<div class="mb-4">
<%= form.label :password, class: "block text-gray-700 text-sm font-bold mb-2" %>
<%= form.password_field :password, autocomplete: "new-password", class: "shadow border-gray-200 rounded w-full" %>
</div>
<div class="mb-6">
<%= form.label :password_confirmation, class: "block text-gray-700 text-sm font-bold mb-2" %>
<%= form.password_field :password_confirmation, autocomplete: "new-password", class: "shadow border-gray-200 rounded w-full" %>
</div>
<%= form.button "Sign Up", class: "font-bold py-2 px-4 mb-4 rounded bg-blue-500 hover:bg-blue-700 text-white w-full" %>
<p class="text-center text-sm text-gray-600 max-w">
Already have an account?
<%= link_to "Sign in", new_user_session_path, class: "font-medium text-blue-600 hover:text-blue-800" %>
</p>
<% end %>
<% end %>
</div>
</div>
スタイルが反映されていれば完了です。
fields_for
を用いることでUser
の属性と同時にAccount
の属性であるname
もaccount_name
としてパラメータに含めることができます。
動作確認
実際に登録してみます。
ログを見てみると、パラメータの"user"の値の中にaccount_attributesという形でaccountの属性が含まれています。
Parameters: {"authenticity_token"=>"[FILTERED]", "user"=>{"account_attributes"=>{"name"=>"TEST01 inc."}, "email"=>"user@example.com", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "button"=>""}
コンソールで確認すると、user, accountが紐づいた状態で作成されています。
irb(main):009> User.count
User Count (1.1ms) SELECT COUNT(*) FROM "users"
=> 1
irb(main):010> Account.count
User Count (1.1ms) SELECT COUNT(*) FROM "accounts"
=> 1
irb(main):011> User.last.account.id == Account.last.id
User Load (3.7ms) SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT $1 [["LIMIT", 1]]
Account Load (0.5ms) SELECT "accounts".* FROM "accounts" WHERE "accounts"."id" = $1 LIMIT $2 [["id", 4], ["LIMIT", 1]]
Account Load (0.2ms) SELECT "accounts".* FROM "accounts" ORDER BY "accounts"."id" DESC LIMIT $1 [["LIMIT", 1]]
=> true
テストを追加
最後にRSpec
を追加します。
spec/system/users
ディレクトリを追加し、registrations_spec.rb
ファイルを以下の内容で作成します。
require "rails_helper"
RSpec.describe "Registrations", type: :system do
context "When the information is entered correctly" do
it "A new user can be registered, and an account linked to user is created" do
visit new_user_registration_path
fill_in "user_account_attributes_name", with: "TEST inc."
fill_in "user_email", with: "user@example.com"
fill_in "user_password", with: "password"
fill_in "user_password_confirmation", with: "password"
expect do
click_button "Sign Up"
expect(page).to have_current_path new_user_session_path
end.to change(User, :count).by(1).and change(Account, :count).by(1)
user = User.last
account = Account.last
expect(user.account).to eq(account)
end
end
end
正常にテストが実行できれば完了です。
Registrations
When the information is entered correctly
A new user can be registered, and an account linked to user is created
Finished in 0.93563 seconds (files took 1.04 seconds to load)
1 example, 0 failures
参考