devise の model 分割をチャレンジしてみた(with docker-compose)
split devise models ...
TL;DR
- devise のくだりのあたりはこの記事とほぼ同じです
- docker-compose 下で rails new して Rails6.1+deviseを試す | 北山淳也 | zenn
- Rails6.1+devise の controller と view をカスタマイズできる状態にする | 北山淳也 | zenn
- devise の model 分割の話はこの記事紹介で書いてます
- 記事紹介:devise のモデルを分割して単一責任の原則を導入する | 北山淳也 | zenn
- 今回のリポジトリはこちらです
- (記事が長くなってしまったので、リポジトリみてもらった方が早いかも)
- https://github.com/JUNKI555/rails_authentication_practice03
devise の model 分割 について
以前記事紹介の記事あげたんですが、メリットなどはそちらを参照してもらえると。
以前記事紹介した時は腹落ちしてなかったんですがちょっとわかってきたんで今回記事にしました。
ただ、まだまだうまくいかない部分も残ってます……(後述)
- パーフェクトRails著者が解説するdeviseの現代的なユーザー認証のモデル構成について | joker1007’s diary
- 記事紹介:devise のモデルを分割して単一責任の原則を導入する | 北山淳也 | zenn
今回の記事でこの話が始まるのは
「認証認可用のモデルを作成」の頁からなんで、そこまで読み飛ばしてもらってOKとも思います。
作成するmodelの名前とかはなるべく記事に合うような形にしています。
docker-compose の準備, bundle init して rails new する
前述の過去記事と同じですが今回は久しぶりに手順を追っていってみます。
サクッとやっていきましょう。
cd myapp
mkdir -p forDocker/mysql/conf.d/
touch forDocker/mysql/conf.d/mysql.cnf
mkdir -p forDocker/rails/
touch forDocker/rails/entrypoint.sh
touch Dockerfile
touch docker-compose.yml
[mysqld]
default_authentication_plugin = mysql_native_password
skip-host-cache
skip-name-resolve
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
init-connect = SET NAMES utf8mb4
skip-character-set-client-handshake
[client]
default-character-set = utf8mb4
[mysqldump]
default-character-set = utf8mb4
[mysql]
default-character-set = utf8mb4
#!/bin/bash
set -e
# Reference by https://matsuand.github.io/docs.docker.jp.onthefly/compose/rails/
# Remove a potentially pre-existing server.pid for Rails.
rm -f /app/tmp/pids/server.pid
# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"
FROM ruby:2.7
RUN set -x && curl -sL https://deb.nodesource.com/setup_14.x | bash -
RUN set -x && apt-get update -y -qq && apt-get install -yq less lsof vim default-mysql-client
RUN set -x && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo 'deb http://dl.yarnpkg.com/debian/ stable main' > /etc/apt/sources.list.d/yarn.list
RUN set -x && apt-get update -y -qq && apt-get install -yq nodejs yarn
RUN mkdir /app
WORKDIR /app
# ※docker-compose up で rails s するときはここのコメントを外す
# COPY Gemfile /app/Gemfile
# COPY Gemfile.lock /app/Gemfile.lock
# RUN bundle install
COPY . /app
# Add a script to be executed every time the container starts.
COPY ./forDocker/rails/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000
# Start the main process.
# ※docker-compose up で rails s するときはここのコメントを外す
# CMD ["rails", "server", "-b", "0.0.0.0"]
version: '3.8'
services:
app:
container_name: app
build: .
tty: true
stdin_open: true
volumes:
- .:/app
- bundle_install:/usr/local/bundle
ports:
- "3000:3000"
depends_on:
- db
db:
platform: linux/x86_64
image: mysql:8.0
container_name: db
restart: always
volumes:
- ./forDocker/mysql/conf.d:/etc/mysql/conf.d
- dbvol:/var/lib/mysql
ports:
- "3306:3306"
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 1
TZ: "Asia/Tokyo"
volumes:
bundle_install:
dbvol:
Dockerfile で CMD ["rails", "server", "-b", "0.0.0.0"]
の部分をコメントアウトしてるので
正確にはこのタイミングではまだ forDocker/rails/entrypoint.sh は不要なんですが
あっても特に問題ないので大丈夫です。
docker-compose.yml はこれまでの記事から少し変えています
- ファイルを
version: '3.8'
に -
bundle_install:/usr/local/bundle
を追加-
bundle install
先を永続化することでbundle install --path foo
の指定をなくし、毎回bundle install
する必要をなくして起動時間を短縮 - 公式のRubyのDockerイメージのbundlerの挙動 | freedom-man.com
- Rails の Dockerfile の自分なりのベストプラクティス | kawabatas技術ブログ
-
-
mysql:8.0
image にplatform: linux/x86_64
を追記- M1 Mac で動く image を pull してくるにはこの指定をすればいいようです
- image の指定に DIGESTの欄のIDを指定する方法もネット上にありますが、この指定でイケました
- mysql の image がマルチCPUアーキテクチャサポートでARMに対応してないから……?正確な理由は理解できてないです
- platform | Compose file version 2 reference | Docker Documentation
- "This parameter determines which version of the image will be pulled and/or on which platform the service’s build will be performed."
- https://docs.docker.com/compose/compose-file/compose-file-v2/
- Docker (Apple Silicon/M1 Preview) MySQL "no matching manifest for linux/arm64/v8 in the manifest list entries" | Stack Overflow
- M1 Mac で動く image を pull してくるにはこの指定をすればいいようです
では初期設定をしていきましょう。
この時点で作業ディレクトリを VSCode とかで開いておきながら以下ターミナルの作業を開始すると楽です。
# サービス用のポート(3000)を有効化し、ホスト側に割り当て可能にして起動
docker-compose run --rm --service-ports app bash
# bashログインできてもちょっと待つ(MySQLが立ち上がってくるのを待つ)
# mysql にパスワードなし root で接続できるか確認
mysql -u root -h db -e 'select version();'
# bundle config 確認 BUNDLE_APP_CONFIG: "/usr/local/bundle" なのを確認
bundle config
# Gemfile 作成
bundle init
# rails 追加 (Gemfile を直接編集してから bundle install でもOK)
bundle add rails --version "~> 6.1.2.1"
bundle install
# rails new
# -B:bundle installしない, -S:sprocketsを組み込まない
# -T:test::unitを組み込まない, -J:javascriptを組み込まない
# -d myaql:データベースの種類, --force:ファイルが存在する場合に上書き
rails new . -B -S -T -J -d mysql --force
bundle install
OKです。今回はついでに simpacker を入れちゃいます。
ターミナルはそのままにしておいて、Gemfile に simpacker
を追記して
...(前略)
gem "simpacker"
...(後略)
でまたターミナルで
# simpacker を入れる
bundle install
# simpakcer 初期化
rails simpacker:install
webpack は消します。
ターミナルはそのままにしておいて、 package.json の devDependencies はいったん空に。
package.json
{
"private": true,
"devDependencies": {
}
}
rails simpacker:install すると npm で入ってしまうので
不要なファイルを消して yarn しなおします。
でまたターミナルに戻って
rm -rf node_modules/
rm package-lock.json
rm webpack.config.js
yarn install
これで余計な package のない Simpacker がセットアップできます。
git 管理しているときは .gitignore に /node_modules/*
も追記しておきましょう。
ここまで終わったら次は Rails アプリケーションの準備を整えましょう
ターミナルはそのままにしておいて、 config/database.yml を編集します。
...(前略)
default: &default
adapter: mysql2
encoding: utf8mb4
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
url: mysql2://root:@db:3306
# username: root
# password:
# host: localhost
...(後略)
config/database.yml の編集を行ったら、データベースの準備をしてrailsを起動してみます。
でまたターミナルに戻って
rails db:create
rails db:migrate
rails s -b "0.0.0.0"
http://localhost:3000/ で "Yay! You’re on Rails!" っていういつものアレが出ましたでしょうか。
OKですね。Ctrl+c でサーバを閉じましょう。
devise を導入する
ターミナルはそのままにしておいて、 Gemfile に以下を追加。
ついでに日本語化に必要な gem と開発環境用のメール確認gem、
スキーマ情報をmodelに出力するgem、action_args も入れちゃいます。
# 認証認可
gem 'devise'
gem 'devise-i18n'
# コントローラーアクション引数パラメーター化
gem 'action_args'
group :development do
# 開発環境用送信メール確認
gem 'letter_opener_web'
# スキーマ情報出力
gem 'annotate'
...(後略)
Gemfile を編集したら、またターミナルに戻って
bundle install
# setup annotate
rails g annotate:install
# setup devise
rails g devise:install
bundle exec rails g devise:install
すると
以下のように手作業での導入方法が表示されますね。
Running via Spring preloader in process 1022
create config/initializers/devise.rb
create config/locales/devise.en.yml
===============================================================================
Depending on your application's configuration some manual setup may be required:
1. Ensure you have defined default url options in your environments files. Here
is an example of default_url_options appropriate for a development environment
in config/environments/development.rb:
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
In production, :host should be set to the actual host of your application.
* Required for all applications. *
2. Ensure you have defined root_url to *something* in your config/routes.rb.
For example:
root to: "home#index"
* Not required for API-only Applications *
3. Ensure you have flash messages in app/views/layouts/application.html.erb.
For example:
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
* Not required for API-only Applications *
4. You can copy Devise views (for customization) to your app by running:
rails g devise:views
* Not required *
===============================================================================
では提示された手順どうり準備を進めていきましょう。
1. 開発環境用のメール送信の設定を行う
ターミナルはそのままにしておいて、 config/environments/development.rb に以下を追記します。
letter_opener_web の設定も一緒に入れちゃいます。
どこでも良いですが私はこの辺に追記します。
...(前略)
# Store uploaded files on the local file system (see config/storage.yml for options).
config.active_storage.service = :local
# ----- ここを追記 -----
# Devise mailer setting
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
config.action_mailer.delivery_method = :letter_opener_web
# ----- ここまで -----
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
...(後略)
2の準備. rails g controller で生成するファイルの種類を変更
root_url となる View を作成する前に、
rails g controller で生成するファイルの種類を変更しておきましょう。
helper とかいらんねん。(使うときは自分で作ればいい)
config/application.rb を編集します。
module App
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 6.1
...(中略)
# ----- ここを追記 -----
# rails generate で生成するファイルを変更
config.generators do |g|
g.assets false # CSS/JSファイル生成せず
g.skip_routes false # trueなら routes.rb変更せず
g.helper false # ヘルパー生成せず
end
# ----- ここまで -----
end
2. root_url となる View を作成する
今回は home にしましょう。
またターミナルに戻って
rails g controller Home index
# 出力
Running via Spring preloader in process 1143
create app/controllers/home_controller.rb
route get 'home/index'
invoke erb
create app/views/home
create app/views/home/index.html.erb
先の設定で helper や CSS が出力されませんね。OKです。
ターミナルはそのままにしておいて、 config/routes.rb で
ルートの定義を編集します。
ついでに letter_opener_web の設定も入れちゃいます。
Rails.application.routes.draw do
get 'home/index'
root to: "home#index"
## 開発環境用letter_opener
mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end
3. app/views/layouts/application.html.erb にメッセージ追加
……と思いましたが、
後で routes.rb でルーティングの設定を行うのでその後にしましょう。
先に config/initializers/devise.rb を少し編集しておきます
# The default HTTP method used to sign out a resource. Default is :delete.
config.sign_out_via = :get # ここを :delete から :get に編集
認証認可用のモデルを作成
さてようやく本題です。
ターミナルに戻って作業を進めます。
まずは rails g devise User
で下地を作っちゃいます。
rails g devise User
## 出力
Running via Spring preloader in process 1186
invoke active_record
create db/migrate/20210212083404_devise_create_users.rb
create app/models/user.rb
insert app/models/user.rb
route devise_for :users
migration ファイルのファイル名は各々変わると思うので rails g devise User した時の出力などを見てみてください。
で、今回はこの db/migrate/20210212083404_devise_create_users.rb を
めっちゃシンプルにしちゃいます。こんな感じ。ほぼ情報ないテーブルですね。
# frozen_string_literal: true
class DeviseCreateUsers < ActiveRecord::Migration[6.1]
def change
create_table :users do |t|
t.string :nickname, null: false
t.timestamps null: false
end
end
end
では devise として必要な情報はどうするのか?
他のmodelに用意していきます。またターミナルに戻って、
# サインアップ用のmodel
rails g devise UserRegistration
# 出力
invoke active_record
create db/migrate/20210212093314_devise_create_user_registrations.rb
create app/models/user_registration.rb
insert app/models/user_registration.rb
route devise_for :user_registrations
# email+password 認証model
rails g devise UserDatabaseAuthentication
# 出力
invoke active_record
create db/migrate/20210212093400_devise_create_user_database_authentications.rb
create app/models/user_database_authentication.rb
insert app/models/user_database_authentication.rb
route devise_for :user_database_authentications
それぞれの migration ファイルはこんな感じに編集します。
# frozen_string_literal: true
class DeviseCreateUserRegistrations < ActiveRecord::Migration[6.1]
def change
create_table :user_registrations do |t|
# user_registrations は users と関連付いている必要はない
## Confirmable
t.string :confirmation_token, null: false
t.datetime :confirmed_at
t.datetime :confirmation_sent_at
t.string :unconfirmed_email # Only if using reconfirmable
t.string :email
t.timestamps null: false
end
add_index :user_registrations, :confirmation_token, unique: true
add_index :user_registrations, :unconfirmed_email, unique: true
end
end
# frozen_string_literal: true
class DeviseCreateUserDatabaseAuthentications < ActiveRecord::Migration[6.1]
def change
create_table :user_database_authentications do |t|
t.references :user, foreign_key: true, dependent: :destroy, index: {unique: true}
## Database authenticatable
t.string :email, null: false
t.string :encrypted_password, null: false
t.timestamps null: false
end
add_index :user_database_authentications, :email, unique: true
end
end
devise の各モジュールごとの責任を追った model を定義することで
それぞれの model が管理する情報を極力絞っています。
で、各modelのrbファイルはこんな感じに。
class User < ApplicationRecord
devise :authenticatable
has_one :user_database_authentication, dependent: :destroy
end
class UserRegistration < ApplicationRecord
devise :confirmable
end
class UserDatabaseAuthentication < ApplicationRecord
devise :database_authenticatable, :validatable, :registerable, :recoverable
belongs_to :user
end
これでOKです。ではターミナルに戻って rails db:migrate
しましょう。
rails db:migrate
OKですね。
なお、rails db:migrate
を実行すると gem 'annotate'
を導入してるので、model にはスキーマ情報が出力されているはずです。
便利ですね。
view と controller を準備する
続けてターミナルで view と controller を準備していきます。
# i18n 対応の view 生成
rails g devise:i18n:views users
# 出力
invoke Devise::I18n::SharedViewsGenerator
create app/views/users/shared
create app/views/users/shared/_error_messages.html.erb
create app/views/users/shared/_links.html.erb
invoke Devise::I18n::MailerViewsGenerator
create app/views/users/mailer
create app/views/users/mailer/confirmation_instructions.html.erb
create app/views/users/mailer/email_changed.html.erb
create app/views/users/mailer/password_change.html.erb
create app/views/users/mailer/reset_password_instructions.html.erb
create app/views/users/mailer/unlock_instructions.html.erb
invoke i18n:form_for
create app/views/users/confirmations
create app/views/users/confirmations/new.html.erb
create app/views/users/passwords
create app/views/users/passwords/edit.html.erb
create app/views/users/passwords/new.html.erb
create app/views/users/registrations
create app/views/users/registrations/edit.html.erb
create app/views/users/registrations/new.html.erb
create app/views/users/sessions
create app/views/users/sessions/new.html.erb
create app/views/users/unlocks
create app/views/users/unlocks/new.html.erb
# controller 生成
rails g devise:controllers users
# 出力
create app/controllers/users/confirmations_controller.rb
create app/controllers/users/passwords_controller.rb
create app/controllers/users/registrations_controller.rb
create app/controllers/users/sessions_controller.rb
create app/controllers/users/unlocks_controller.rb
create app/controllers/users/omniauth_callbacks_controller.rb
===============================================================================
Some setup you must do manually if you haven't yet:
Ensure you have overridden routes for generated controllers in your routes.rb.
For example:
Rails.application.routes.draw do
devise_for :users, controllers: {
sessions: 'users/sessions'
}
end
===============================================================================
# view の日本語訳をいじれるように locale ファイルを出力しておく
rails g devise:i18n:locale ja
create config/locales/devise.views.ja.yml
続いて先ほど生成した config/locales/devise.views.ja.yml を少し編集します。
ja:
activerecord:
attributes:
user:
confirmation_sent_at: パスワード確認送信時刻
confirmation_token: パスワード確認用トークン
nickname: 名前 #← nickname を user 配下に追加。このコメントは実際は不要
...(中略)
users: #← devise を users に変更。このコメントも実際は不要
confirmations:
confirmed: メールアドレスが確認できました。
config/locales/devise.views.ja.yml の devise
を users
に変えるのは
エイリアスなどでも対応できるはずですが、自分はうまく動きませんでした。
以下記事を参考に試してみるのも良いでしょう。
- 【Rails】devise-i18nでscopedなビューを翻訳するワザ | Qiita
- Devise Viewをカスタマイズするとdevise-i18nが効かない | myMemoBlog by 256hax
では次に日本語のlocale設定も入れておきましょう。
config/application.rb を開いて
...(前略)
module App
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 6.1
# ----- ここを追記 -----
# Devise i18n japanese
config.i18n.default_locale = :ja
# ----- ここまで -----
...(後略)
routes.rb を編集する
devise の model を分割し、
devise の module はそれぞれの model が持っている形になっているので
routes.rb の書き方が通常とは変わります。
devise の module についてはこの記事にまとめてあります。
- gem devise の module 翻訳 | 北山淳也 | zenn
ではやっていきましょう。こんな感じです。
Rails.application.routes.draw do
devise_for :user_registrations, path:'users', :controllers => {
:confirmations => 'users/confirmations',
}
devise_for :user_database_authentications, path:'users', :controllers => {
:passwords => 'users/passwords',
:registrations => 'users/registrations',
:sessions => 'users/sessions',
}
# これは実際いらないかも……?
devise_for :users
get 'home/index'
root to: "home#index"
## 開発環境用letter_opener
mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end
確認メールを送信する devise :confirmable
は UserRegistration
model が持っているので、 'users/confirmations'
は UserRegistration
と紐付けます。
メール認証やパスワード確認を行う devise :database_authenticatable, :validatable
は UserDatabaseAuthentication
model が持っているので紐付けています。
この状態の routes.rb にすると
例えば通常の devise setup で new_use_confirmation_path
となるものが
new_user_registration_confirmation_path
となっています。
rails routes
コマンドや http://localhost:3000/rails/info/routes
などでルーティングの割り当て状況が確認できるので確認しておきましょう。
app/views/layouts/application.html.erb にメッセージ追加
ルーティングが決定したのでリンクを貼っていきます。
こんな感じです。
<!DOCTYPE html>
<html>
<head>
<title>App</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all' %>
</head>
<body>
<header style="margin-top:30px;">
<nav>
<!-- user_signed_in? はユーザがログインしているか調べる devise の Helper メソッド -->
<% if user_signed_in? %>
<!-- current_user は現在ログインしているUserオブジェクトを返す devise の Helper メソッド -->
<!-- *_path はUserモデルを作成したときに、deviseにより自動で作成されてますので、rake routes で確認できます -->
Logged in as <strong><%= current_user.user_database_authentication.email %></strong>.
<%= link_to 'プロフィール変更', edit_user_database_authentication_registration_path %> |
<%= link_to "ログアウト", destroy_user_database_authentication_session_path %>
<% else %>
<%= link_to "サインアップ", new_user_registration_confirmation_path %> |
<%= link_to "ログイン", new_user_database_authentication_session_path %>
<% end %>
</nav>
</header>
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
<%= yield %>
</body>
</html>
パスの指定が通常のdevise setup 時と違うのがわかると思います。
なお、パス指定が変わっているので生成された各view に存在する
<%= link_to t('devise.shared.links.back'), :back %>
の部分は
一旦消しといてやると作業が楽です。
(もちろん <%= link_to t('devise.shared.links.back'), :back %>
を動くように改修してもよい)
controller と view の設定をしていく
では controller と view をmodel分割に対応した形に設定していきます。
確認メール送信部分:confirmations_controller.rb
app/controllers/users/confirmations_controller.rb です。
ポイントとしては以下3点で、
-
model 分割しない devise では、
users/confirmations#create
が実行される時に
User model があることを前提とした動きをするので、
super
を呼ぶ前にUserRegistration
を作っておいてやります。 -
確認メールを送信した後に使用する path は root にする(これは必須ではない)
-
確認メールのリンクをクリックした時に参照される
/users/confirmation?confirmation_token=abcdef
のリダイレクト先は
new_user_database_authentication_registration_path
となります。
以上を踏まえて編集するとこんな感じです。
# frozen_string_literal: true
class Users::ConfirmationsController < Devise::ConfirmationsController
# GET /users/confirmation/new
def new
super
end
# POST /users/confirmation
def create()
registered = UserDatabaseAuthentication.where(email: params[:user_registration][:email]).exists?
if registered
flash[:error] = '既に登録されているメールアドレスです。'
return render :new
end
user_registration = UserRegistration.find_or_initialize_by(unconfirmed_email: params[:user_registration][:email])
if user_registration.save
super
else
flash[:error] = '処理に失敗しました。もう一度お試しください。'
return render :new
end
end
# GET /users/confirmation?confirmation_token=abcdef
def show
# super 側で confirmation_token の検証をしてリダイレクト
super
end
protected
# 確認メールを送信した後に使用する path
def after_resending_confirmation_instructions_path_for(resource_name)
root_path
end
# 確認後に使用する path
def after_confirmation_path_for(resource_name, resource)
new_user_database_authentication_registration_path
end
end
users/confirmations#new
部分の view も少し編集しておきます。
confirmation_path(resource)
の部分がどうしてもエラーになってしまったので
パスを直接記述するようにしました。
<h2><%= t('.resend_confirmation_instructions') %></h2>
<%= form_for(resource, as: resource_name, url: user_registration_confirmation_path, html: { method: :post }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %>
</div>
<div class="actions">
<%= f.submit t('.resend_confirmation_instructions') %>
</div>
<% end %>
なお、 gem 'letter_opener_web'
を導入しているので、
確認メール送信後は http://localhost:3000/letter_opener
で確認できるはずです。
アカウント作成部分:registrations_controller.rb
先に users/registrations#new
の view を編集して nickname をコントローラー側で受け取れるようにしてやります。
余談ですが、こっちの registration_path(resource_name)
は動きました。
さっきとの違いがわからない……💦
<h2><%= t('.sign_up') %></h2>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<div class="field">
<%= f.label :nickname %><br />
<%= text_field_tag(:nickname) %>
</div>
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email, autocomplete: "email" %>
</div>
<div class="field">
<%= f.label :password %>
<% if @minimum_password_length %>
<em><%= t('devise.shared.minimum_password_length', count: @minimum_password_length) %></em>
<% end %><br />
<%= f.password_field :password, autocomplete: "new-password" %>
</div>
<div class="field">
<%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation, autocomplete: "new-password" %>
</div>
<div class="actions">
<%= f.submit t('.sign_up') %>
</div>
<% end %>
では app/controllers/users/registrations_controller.rb です。
users/registrations#create
で
nickname
を受け取り User
を作成し、UserDatabaseAuthentication
も作成してやります。パスワードの検証は UserDatabaseAuthentication
を save
するときに devise がうまくやってくれます。(この辺りの devise のコードを読み解くのが大変でした……)
また、sign_in()
は User
と UserDatabaseAuthentication
両方で実施するように変更します。
こんな感じです。
# 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 /users/sign_up
def new
super
end
# POST /users
def create
ActiveRecord::Base.transaction do
@user = User.new(nickname: params[:nickname])
@user.save!
@user_database_authentication = \
UserDatabaseAuthentication.new( \
user: @user, \
email: params[:user_database_authentication][:email], \
password: params[:user_database_authentication][:password], \
password_confirmation: params[:user_database_authentication][:password_confirmation])
@user_database_authentication.save!
end
sign_in(:user, @user)
sign_in(:database_authentication, @user_database_authentication)
redirect_to root_path
rescue
flash[:alert] = '処理に失敗しました。もう一度お試しください。'
return render :new
end
# GET /users/edit
def edit
super
end
# PUT /users
def update
# 更新処理しようとするとなぜか user をSELECTして例外になる
# if !resource.valid_password?(account_update_params[:current_password])
# flash[:alert] = 'Current password が間違っています'
# return render :edit
# end
# resource.update_with_password(account_update_params)
# flash[:notice] = '更新しました'
# redirect_to root_path
# rescue
# flash[:alert] = '処理に失敗しました。もう一度お試しください。'
# return render :edit
end
# DELETE /users
def destroy
super
end
# GET /users/cancel
# 通常はサインイン後に
# 期限切れになるセッションデータを強制的に今すぐ期限切れにします。
# これは、ユーザーがすべての OAuth セッションデータを削除して、
# 途中で oauth サインイン/アップをキャンセルしたい場合に便利です。
def cancel
super
end
protected
# 許可するための追加のパラメータがある場合は、sanitizer に追加してください
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute])
end
# 許可するための追加のパラメータがある場合は、sanitizer に追加してください
def configure_account_update_params
devise_parameter_sanitizer.permit(:account_update, keys: [:attribute])
end
# サインアップ後に使用する path
def after_sign_up_path_for(resource)
super(resource)
end
# アクティブでないアカウントのサインアップ後に使用する path
def after_inactive_sign_up_path_for(resource)
super(resource)
end
end
なお、 パスワード変更画面からPUTされる users/registrations#update
については
どうしてもうまく実装する事ができませんでした……🙇♂️ ので、コメントアウトしてあります。
ログイン機能:sessions_controller.rb
最後に app/controllers/users/sessions_controller.rb を改変します。
ここの改変は簡単で、
先ほどと同様に また、sign_in()
が User
と UserDatabaseAuthentication
両方で実施するように変更します。
routes.rb の設定で sessions_controller.rb での devise としての resource は UserDatabaseAuthentication
model になっているので、super
側で
UserDatabaseAuthentication
の sign_in()
は実行されています。
なので、 User
側の sign_in()
だけ追加してやりましょう。こんな感じです。
# frozen_string_literal: true
class Users::SessionsController < Devise::SessionsController
before_action :configure_sign_in_params, only: [:create]
# GET /users/sign_in
def new
super
end
# POST /users/sign_in
def create
super do |resource|
sign_in(:user, resource.user)
end
end
# DELETE /users/sign_out
def destroy
super
end
protected
# If you have extra params to permit, append them to the sanitizer.
def configure_sign_in_params
devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute])
end
end
以上
これで最低限「確認メール送信」「アカウント作成」「ログイン」「ログアウト」は動くようになりました!
今回、なるべく devise で生成される controller と view を生かした感じにするよう
devise の model 分割にチャレンジしてみましたが、
かなり難しかったです…… パスワード更新の部分などうまくいってない部分もあるので、
また時間を見つけて再チャレンジしたいなと思っています。
devise の model 分割に詳しい方はコメントももらえると とても助かります💦
長い記事を読んでくださりありがとうございました。
今回のリポジトリはこちらです。
Discussion