Rails6.1 + Devise 3.8.0 で、Deviseのデフォルトの挙動を確認してみた

Deviseを使うと認証まわりの機能をRailsに組み込むのが容易です。

ただ、「そういえばDeviseにある各モジュールのデフォルトの挙動はどんな感じだろう」と思ったため、調べた時のメモを残します。

 
目次

 

環境

  • Rails 6.1.3.2
  • Devise 3.8.0

 

検証するためのRails + Deviseアプリを作成

今回使用するモジュールは

  • database_authenticatable
  • registerable
  • recoverable
  • confirmable
  • validatable
  • lockable

です。

ただ、各モジュールの設定はデフォルトのままではなく、気になった設定はカスタマイズしていきます。

具体的には以下です。

  • 変更時にもconfirmableを動作させる
  • Deviseが送信するメール中のトークンを見やすくするため、DeviseのMailerとビューを差し替え
  • 動作確認をしやすいよう、以下の設定へと変更
    • 確認トークンの有効期限を1分
    • ログイン失敗は3回まで
    • ログイン失敗のロックは1分間
    • パスワードリセットで生成するトークンの有効期限は1分

 

アプリ作成

今回は rails_devise_default_app という名前でRailsアプリを作成します。

% rails new rails_devise_default_app

 

Gemfileの修正

各モジュールのViewについては必要最低限とするため、今回不要な webpaker をGemfileを削除します。

一方、今回検証する Devise と、Deviseが送信するメールをブラウザで確認するための Letter Opener を追加します。

# コメントアウト
# gem 'webpacker', '~> 5.0'

# 追加
gem 'devise'

group :development do
  gem 'letter_opener'
end

 
変更したので、インストールします。

% bundle install

 

RailsのControllerとViewの作成

Deviseでログインしている時の挙動を確認するため、

  • 誰でもアクセスできるページ
    • Controllerの index メソッドを使う
  • ログインしたユーザーのみアクセスできるページ
    • Controllerの show メソッドを使う

の2つを用意します *1

% bin/rails g controller home index show --helper=false --assets=false
Running via Spring preloader in process 40269
      create  app/controllers/home_controller.rb
       route  get 'home/index'
get 'home/show'
      invoke  erb
      create    app/views/home
      create    app/views/home/index.html.erb
      create    app/views/home/show.html.erb
      invoke  test_unit
      create    test/controllers/home_controller_test.rb

 
show だけログイン必須とするため、 Controller filter の before_action で Devise の authenticate_user!show メソッドのみ動作するよう定義します。
https://github.com/heartcombo/devise#controller-filters-and-helpers

class HomeController < ApplicationController
  before_action :authenticate_user!, only: ['show']

  def index
  end

  def show
  end
end

 
Viewのうち、index (app/views/home/index.html.erb) は誰でもアクセスできるページのため

<h1>Public Page</h1>

とします。

一方、 show (app/views/home/show.html.erb) はログイン済ユーザーのみアクセス可能なため、その旨をViewにも表示します。

<h1>Private Page</h1>
<p>only signed in users</p>

 
ただ、これだけだとログインしているユーザーが誰か分からないため、共通で使う layout (app/views/layouts/application.html.erb) でユーザー情報を表示させます。

なお、layoutには Webpacker のタグが記載されていたため、そちらは削除しておきます。

<head>
  ...
  <%# webpacker を使用していないため、不要なものを削除 %>
  <%#= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>

<body>
  <%# deviseを使ったログインができているかを確認しやすくするため、追加 %>
  <% if current_user.present? %>
    <p>ログイン済ユーザ: <%= current_user.email %></p>
  <% end %>
  <p class="notice"><%= notice %></p>
  <p class="alert"><%= alert %></p>
  <%# 追加ここまで %>

  <%= yield %>
</body>
</html>

 

Railsの起動するポートを変更

本来この設定を行う必要はないのですが、手元でRailsアプリが増えてしまっているので、app/config/puma.rb を修正し、今回のアプリが起動するポートを 3710 へと変更しておきます。

port ENV.fetch("PORT") { 3710 }

 

Action Mailerまわりの修正

設定ファイル config/environment/development.rb に対し、Action Mailerまわりについて、Railsの起動ポートと Letter Opener を考慮した形で、末尾に追記します。

# 追加
# メール内のリンクをRailsの起動ポートと合わせる
config.action_mailer.default_url_options = { host: 'localhost', port: 3710 }

# Letter Openerを使うように設定
config.action_mailer.delivery_method = :letter_opener

# Letter Openerで確認するため、実際のメール配信を行う
config.action_mailer.perform_deliveries = true

 

Deviseのジェネレータで初期化

bin/rails g devise:install を実行します。

% bin/rails g devise:install

Running via Spring preloader in process 94335
      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 *

 

Devise用の User Model を作成

Deviseのジェネレータを使って生成します。

% bin/rails g devise User
Running via Spring preloader in process 41318
      invoke  active_record
      create    db/migrate/20210605140140_devise_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      insert    app/models/user.rb
       route  devise_for :users

 
続いて、User Model (app/models/user.rb) に Devise のモジュールを組み込みます。

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  # devise :database_authenticatable, :registerable,
  #        :recoverable, :rememberable, :validatable
  #
  # 変更
  devise :database_authenticatable, # 認証
         :registerable, # 登録・変更・削除
         :recoverable, # パスワードリセット
         :confirmable, # メールでの登録
         :validatable, # メールやパスワードのバリデーション
         :lockable # アカウントロック
end

 

マイグレーションファイルの編集

Deviseのジェネレータが生成したマイグレーションファイル (db/migrate/<タイムスタンプ>_devise_create_user.rb) に対して、以下の編集を行います。

  • 必要なDeviseモジュールの項目やIndexを有効化
  • Deviseとは関係ないフィールド name を用意
# frozen_string_literal: true

class DeviseCreateUsers < ActiveRecord::Migration[6.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.timestamps null: false

      # 追加した項目
      t.string :name,              null: false, default: ""
    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

 
マイグレーションファイルの編集が終わったら、マイグレーションを実行しておきます。

% bin/rails db:migrate

 

User向けにDeviseのViewを生成

今回使わないものが多いですが、DeviseのViewをジェネレータで生成しておきます。

% bin/rails g devise:views users
Running via Spring preloader in process 40901
      invoke  Devise::Generators::SharedViewsGenerator
      create    app/views/users/shared
      create    app/views/users/shared/_error_messages.html.erb
      create    app/views/users/shared/_links.html.erb
      invoke  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
      invoke  erb
      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

 

DeviseのMaierとメールテンプレートの差し替え

Deviseデフォルトのメールテンプレートには、Deviseが生成した各種トークンが記載されていません。

そこで、目視で確認しやすくするよう、メールのテンプレートを差し替えます。

また、差し替えたメールテンプレートを使うために、DeviseのMailerも差し替えます。

なお、前回の記事でふれた通り、Devise 4.8.0の時点ではWikiのやり方ではメールテンプレートが差し替わらないことに注意します。
Devise 4.8.0 でメールテンプレートを変更したい場合、カスタムメーラーの template_path ではなく headers_for をオーバーライドする - メモ的な思考的な

 
まずは、Deviseのジェネレータで生成したメールテンプレートを差し替えるため、ディレクトapp/views/users/mailer にあるテンプレート

  • confirmation_instructions.html.erb
  • reset_password_instructions.html.erb
  • unlock_instructions.html.erb

の各ファイルを開き、テンプレートの末尾にトークンを表示するよう

<p>token <%= @token %></p>

を追加します。

 
次に、Mailerを差し替えるため、 app/views/mailers/devise_my_mailer.rb として以下を作成します。

class DeviseMyMailer < Devise::Mailer
  def headers_for(action, opts)
    super.merge!(template_path: 'users/mailer')
  end
end

 
最後に、Devise向けの設定ファイル config/initializer/devise.rbconfig.mailer を変更します。

config.mailer = 'DeviseMyMailer'

 

User向けにDeviseのControllerを生成

今回はコントローラのカスタマイズをしませんが、参考までに生成しておきます。

% bin/rails generate devise:controllers users
Running via Spring preloader in process 41089
      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

 

追加したControllerやDeviseのルーティングを追加

config/routes.rb に各ルーティングを追加します。

なお、ルートルーティングはファイルの先頭に書いておきます。
3.14 rootを使う | Rails のルーティング - Railsガイド

Rails.application.routes.draw do
  root to: 'home#index'

  # Deviseのルーティング
  devise_for :users

  # ログインしていないと表示できないページ
  get '/private', to: 'home#show', as: 'private_page'
end

 

動作確認しやすくするよう設定ファイルの修正

動作確認しやすくするため、設定ファイル (config/initializers/devise.rb) を修正します。

# 確認トークンの有効期限
# config.confirm_within = 3.days
config.confirm_within = 1.minute

# ロック回数
# config.maximum_attempts = 20
config.maximum_attempts = 3

# ロック期間 
# config.unlock_in = 1.hour
config.unlock_in = 1.minute

# パスワード忘れの有効期限
# config.reset_password_within = 6.hours
config.reset_password_within = 1.minute

 
以上で、Deviseのデフォルトの挙動を確認するためのアプリができました。

引き続き、Deviseのデフォルトの挙動を見ていきます。スクリーンショット多めです。

 

新規ユーザー登録(サインアップ)について

流れ

今回は bar@example.com ユーザーを登録してみます。

新規ユーザー登録のURLは以下です。
http://localhost:3710/users/sign_up

 
メールとパスワードを入力し、sign upボタンをクリックします。

なお、Deviseデフォルトのテンプレートを修正しない場合、Modelに追加した項目 name は表示されません。

 
ボタンをクリック後、ルートパスに戻り、確認待ちのメッセージが表示されます。

 
一方、メールを確認すると、トークン付URLが記載されたメールが届いています。

 
トークン付URLのリンクをクリックすると、ユーザを認証できました。

また、この時点ではログインできていないため、ログインページへ移動します。

 
先ほど設定したメールアドレスとパスワードを入力すると、ログインできました。

 

ログイン済でのサインアップ・サインイン・パスワードリセットの挙動

ログイン済の状態で

の各ページへアクセスしたところ、ルートパスへリダイレクトしました。

 

メールアドレス変更

流れ

今回は bar@example.com から foo@example.com へメールアドレスを変更してみます。

Deviseのデフォルトでは、ユーザー情報の編集ページでメールアドレスを変更します。
http://localhost:3710/users/edit

 
変更後のメールアドレスと、現在のパスワードを入力し、Updateボタンをクリックします。

なお、Deviseデフォルトのテンプレートを修正しない場合、Modelに追加した項目 name は表示されません。

 
ボタンをクリック後、ルートパスに戻り、確認待ちのメッセージが表示されます。

 
一方、メールを確認すると、トークン付URLが記載されたメールが届いています。

 
メール中のトークン付URLのリンクをログイン中のブラウザで開くと、変更が完了します。ログイン状態も維持されます。

 

メールアドレス確認待ちでの挙動

変更後のメールアドレスが確認待ちの状態の場合、変更後のメールアドレスではログインできません。

 
また、ユーザー情報の編集ページを開くと、現在メールアドレスが変更中と表示されます。

 
ただし、現在メールアドレスが変更中であってもメールアドレスの変更は可能なため、トークン付URLのリンクが含まれるメールが送信されます。

 

変更後のメールアドレスに、変更前と同じメールアドレスを入力した時の挙動

こんな感じで入力します。

 
この場合、トークン付URLが記載されたメールは届かず、変更が完了したと表示されます。

 

パスワード変更について

パスワード変更は、ユーザー情報編集ページで行なえます。

新しいパスワードと現在のパスワードを入力します。

変更されました。ログイン状態は維持されます。

 

パスワードリセットについて

流れ

以下のURLからパスワードのリセットができます。
http://localhost:3710/users/password/new

   
今回、パスワードリセットする時の状態として、

  • ユーザー登録したもののメールアドレスの確認前の場合
  • ユーザー登録に加えメールアドレス確認が完了している場合
  • アカウントがロックされている場合

の3パターンを確認しましたが、いずれも挙動は同じでした。

 
まずはパスワードリセットの画面に、登録済のメールアドレスを入力します。

 
メールを確認すると、トークン付URLが記載されたメールが届いています。

 
画面にもメールを送ったと表示されます。

 
メールのリンクを踏むと、パスワードの再入力画面が表示されます。

 
新しいパスワードを入力し、Change my password ボタンをクリックすると、パスワードが変更されました。

また、ログインも完了しています。

 

パスワードリセットトークンの有効期限が切れた場合

新しいパスワードの入力画面にエラーメッセージが表示されます。

 

パスワードリセット中に、もう一度パスワードリセットを行った場合

新しいパスワードを再入力した際に、トークンが不正というエラーが表示されます。

 

パスワードリセット中に、今までのパスワードでログインしようとした場合

スクリーンショットでの表現は難しいため言葉だけとなりますが、ログインできます。

ただし、パスワードリセットのトークンが有効な間は、トークン付URLからパスワード変更が可能です。

 

アカウントロックについて

流れ

アカウントロックは、Deviseの lockable モジュールになります。

ログイン時にパスワード間違いを続け、ロックまであと1回の状態はこのような状態です。

 
デフォルトでは20回間違えるとアカウントがロックされます。

この後、正しいパスワードを入力したとしても、アカウントはロックされたままです。

 
アカウントがロックされたと同時に、メールアドレスにトークン付URLを含んだメールが届きます。

 
メールのURLをクリックすると、ロックを解除され、ログインできるようになります。

 
正しいメールアドレスとパスワードを入力すると、ログインできました。

 

メール再送について

Deviseでは、メールアドレスの所有者確認やパスワード忘れのメール再送も可能です。

 

メールアドレス確認のメールを再送する場合

Resend confirmation instructions にて再送可能です。
http://localhost:3710/users/confirmation/new

 
サインアップ時のメールアドレス確認の場合でも再送が可能です。

この場合、トークンは同じになります。

1回目

2回目

 

ロック解除メールを再送する場合

こちらは Resend unlock instructions になります。
http://localhost:3710/users/unlock/new

 
メールアドレスの確認とは異なり、再送時のトークンは異なります。

1回目

2回目

 

気になるところ

今まで見てきたとおり、Deviseには色々実装されています。

一方で、気になった部分を書いておきます。

 

別ブラウザでもトークン付URLを踏んだら認証できること

Chromeでユーザ登録を行ってトークン付URLを含むメールを受信した後、トークン付URLをEdgeへコピーしてアクセスしたところ、ユーザの認証ができました。

「同一セッションで認証する」などの制限は行っておらず、トークン付URLを知っていれば誰でも認証できるようです。

既存メールアドレスの存在が分かってしまうこと

デフォルトの状態では、そのアプリケーションにメールアドレスが登録されているかどうかが判断できる作りとなっていました。

 

スクリーンショット
サインアップ時で既存メールアドレスを入力した時

 

メールアドレス変更で既存メールアドレスを入力した時

 

パスワード忘れで、メールアドレスが存在しない時

 

トークン付URLを踏む前のメールアドレスを、サインインで入力した時

 

メールアドレス確認の再送画面で、既に認証済のメールを入力した時

 

メールアドレス確認の再送画面で、メールアドレスが存在しない時

 

ロック解除再送で、ロックしていない時

 

ロック解除再送で、メールアドレスが存在していない時

 

Deviseの paranoid モードを使った対応

Deviseでもこの挙動は認識されています。

DeviseのWiki では paranoid mode を使うと良いと記載されています。
How To: Using paranoid mode, avoid user enumeration on registerable · heartcombo/devise Wiki

 
config/initializer/devise.rb の paranoidtrue とすることで、paranoid modeになります。

config.paranoid = true

 
そこで、paranoid mode を有効化した後の挙動を確認していました。

 

paranoid modeでメールアドレスの存在が分からなくなったケース

サインイン前でも使用可能な画面の

  • パスワード忘れで、メールアドレスが存在しない時
  • メールアドレス確認の再送画面で、既に認証済のメールを入力した時
  • メールアドレス確認の再送画面で、メールアドレスが存在しない時
  • ロック解除再送で、ロックしていない時
  • ロック解除再送で、メールアドレスが存在していない時

については、エラーメッセージが変わり、サインイン画面へと遷移しています。

 

変更なし:サインアップ時で既存のメールアドレスを入力した場合

Wikiにある通り、サインアップ時は変わりません。

 

変更なし:メールアドレス変更で既存のメールアドレスを入力した場合

ログイン済で操作できる画面のせいか、エラーは変わりませんでした。

 

変更なし:トークン付URLを踏む前のメールアドレスを、サインインで入力した時

 

メールアドレス変更で他人のメールアドレスを誤入力すると、他人がアカウントを奪える

例えば、以下の記事にあるような操作をした場合です。
観点2: ユーザが間違って第三者の連絡手段を入力してしまった場合、第三者に個人情報が流出してしまう | Webサービスによくある各機能の仕様とセキュリティ観点(ユーザ登録機能) - Qiita

 

流れ

メールアドレス変更時に、誤ったメールアドレスと正しいパスワードを入力すると、誤ったメールアドレスの持ち主にメールが届きます。

そこで、トークン付URLを踏むと、誤ったメールアドレスへアカウントのメールアドレスが変更されました。しかし、この時点では未ログイン状態です。

 
続いて、パスワードリセット画面へ移動して、メールアドレスを入力します。

 
リセットURL付きのメールがメールアドレスに届きます。

 
続いて、パスワードリセットのURLをクリックします。

 
新しいパスワードを入力します。

 
パスワード変更が完了し、ログインできました。これで、誤ったメールアドレスの持ち主がアカウントを奪えました。

 

徳丸本での記載

徳丸本 第2版 p502 の「メールアドレス変更に必要な機能的対策」によると、

  • 新規メールアドレスによる受信確認
  • 再認証
  • メール通知

このうち、新規メールアドレスによる受信確認を徳丸本 p497 にある方式B (確認番号を入力する方式) にすれば、誤ったメールアドレスを入力しても問題なさそうです。

以下のさきほど挙げた記事でも、似たような内容がふれられています。
Webサービスによくある各機能の仕様とセキュリティ観点(ユーザ登録機能) - Qiita

 
ただ、Deviseの標準機能では、パスワードリセットを確認番号入力化する機能は見当たりませんでした。

そこで、他のWebフレームワークでの実装はあるのか気になりTweetしたところ、徳丸先生に拾っていただけました。ありがとうございます。

 
もし、実装しているWebフレームワークをご存じの方がいれば、教えていただけるとありがたいです。

 

Deviseでの対応 (通知機能)

さて、標準のDeviseではパスワードリセットを確認番号化できないものの、通知機能はあります。
Notify users via email when their passwords change · heartcombo/devise Wiki

上記Wikiにはパスワード変更のみの記載されていますが、Devise 4.8.0 時点ではメールアドレス変更での通知にも対応しています。

 
そこで、devise.rbの以下の設定をアンコメントした上でRailsを再起動し、挙動を確認してみます。

# Send a notification to the original email when the user's email is changed.
config.send_email_changed_notification = true

# Send a notification email when the user's password is changed.
config.send_password_change_notification = true

 

ユーザー情報編集画面の場合

 
ユーザー情報の編集画面では、メールアドレスとパスワードを変更できます。

今回はメールアドレスとパスワードを同時に変更します。

 
変更後、ルートに戻ります。

 
新しいメールアドレスにトークン付URLメールが飛びます。

 
また、メールアドレス変更の通知も飛びます。

トークン付URLをクリックしてメールアドレスの確認をする前ですが通知が飛ぶようです。

 
他に、パスワード変更の通知も飛びます。

 

パスワードリセット画面の場合

パスワードリセットでメールアドレスを入力します。

 
トークン付URLメールが届きますので、パスワードを変更します。

 
変更後、パスワード変更のメールが届きます。

 

参考:Deviseのカスタマイズについて

ここまでDevise標準の機能を見てきました。

もし、Deviseの標準以外の機能を使いたい場合は、自分でカスタマイズすることになります。

カスタマイズ方法については、以下が詳しいです。

 

ソースコード

Githubに上げました。
https://github.com/thinkAmi-sandbox/devise_default_app

*1:DeviseではURLが sign_in となっているものの、Viewでは Login と表示されています。そのため、サインインとログインのどちらを使えばよいか迷いましたが、今回は「ログイン」を使います

Devise 4.8.0 でメールテンプレートを変更したい場合、カスタムメーラーの template_path ではなく headers_for をオーバーライドする

Rails + Deviseにて、Devise標準のメール受信確認用メールテンプレートを差し替えたいことがありました。

 
そこで Devise の Wiki How To: Use custom mailer · heartcombo/devise Wiki に従い、

% bin/rails g devise:views users
...
      create    app/views/users/mailer/confirmation_instructions.html.erb

と View を生成した後、 confirmation_instructions.html.erb

<p>Welcome <%= @email %>!</p>

<p>You can confirm your account email through the link below:</p>

<p><%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></p>

<%# 以下を追加 %>
<p>token => <%= @token %></p>

と変更した上、カスタムメーラー app/mailers/devise_my_mailer.rb

class DeviseMyMailer < Devise::Mailer
  default template_path: 'users/mailer'
end

と定義し、合わせて config/initializer/devise.rb

Devise.setup do |config|
# ...
  config.mailer = 'DeviseMyMailer'
# ...
end

と編集しました。

 
その後、confirmableモジュールを有効にしたRails + Deviseを起動し、ユーザー登録時のメール受信確認を行ったところ、到着したメールには

と、追加したはずのトークンの記載がありませんでした。 template_path で設定したテンプレートではなく Devise 標準のメールテンプレートが使われているように見えました。

そこで調べた時のメモを残します。

 

環境

  • Rails 6.1.3.2
  • Devise 4.8.0

 

対応

issueがありました。
Unable to use custom views for emails · Issue #4842 · heartcombo/devise

また、以下の記事にも解説がありました。
週刊Railsウォッチ(20180820)Railsで構築されたサイト40選、Deviseはつらいよ、ARのスコープとクラスメソッドの使い分けほか|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社

 
そこで、issueに従ってカスタムメーラー app/mailers/devise_my_mailer.rb

class DeviseMyMailer < Devise::Mailer
  # これでは動かない
  # https://github.com/heartcombo/devise/issues/4842
  # default template_path: 'users/mailer'

  def headers_for(action, opts)
    super.merge!(template_path: 'users/mailer')
  end
end

として、再度ユーザー登録時のメール受信確認を行ったところ、到着したメールは想定通り

となっていました。

 
現時点では template_path ではなく headers_for をオーバーライドすると良いようです。

Rails 6.1 + Devise 4.8.0 で、ルーティングから一部のdeviseモジュールの URI Pattern を削除する

Rails + Deviseでは、ModelにDeviseのモジュールを組み込むと、自動的に routes へ URI Pattern が追加されます。

例えば、

Rails.application.routes.draw do
  root to: 'home#index'

  devise_for :users
end

な routes.rb の時、DeviseのModelが

class User < ApplicationRecord
  devise :database_authenticatable,
         :registerable,
         :recoverable,
         # :confirmable,
         :validatable,
         :lockable
end

な場合、

% bin/rails routes
...
    user_registration PATCH  /users(.:format)
                      PUT    /users(.:format)
                      DELETE /users(.:format)
                      POST   /users(.:format)
      new_user_unlock GET    /users/unlock/new(.:format)
          user_unlock GET    /users/unlock(.:format)

となります。

この状態で、Deviseの Confirmableコメントアウトを外し

class User < ApplicationRecord
  devise :database_authenticatable,
         :registerable,
         :recoverable,
         :confirmable,
         :validatable,
         :lockable
end

とすると

% bin/rails routes
...
    user_registration PATCH  /users(.:format)
                      PUT    /users(.:format)
                      DELETE /users(.:format)
                      POST   /users(.:format)
new_user_confirmation GET    /users/confirmation/new(.:format)  # 追加
    user_confirmation GET    /users/confirmation(.:format)  # 追加
                      POST   /users/confirmation(.:format)  # 追加
      new_user_unlock GET    /users/unlock/new(.:format)
          user_unlock GET    /users/unlock(.:format)

になります。

 
ただ、

  • Confirmable モジュールは使いたい
  • URI Pattern を追加したくない

の場合はどうすればよいか迷ったため、メモを残します。

 

目次

 

環境

  • Rails 6.1.3.2
  • Devise 3.8.0

 

対応

routes.rb の devise_forskip を使います。
ruby on rails 3 - How do I remove the Devise route to sign up? - Stack Overflow

 
例えば、Modelが

class User < ApplicationRecord
  devise :database_authenticatable,
         :registerable,
         :recoverable,
         :confirmable,
         :validatable,
         :lockable
end

の時に、routes.rbを

Rails.application.routes.draw do
  root to: 'home#index'

  devise_for :users, 
             skip: ['confirmations']  # 追加
end

とすると、

% bin/rails routes
...
    user_registration PATCH  /users(.:format)
                      PUT    /users(.:format)
                      DELETE /users(.:format)
                      POST   /users(.:format)
      new_user_unlock GET    /users/unlock/new(.:format)
          user_unlock GET    /users/unlock(.:format)

confirmableURI Patternがなくなりました。

Rails6.1で、セッションをキャッシュとは別のmemcachedへ保存する

Rails6.1でセッションをキャッシュとは別のmemcachedへ保存しようとした時、色々調べたことをメモに残します。

 
目次

 

環境

 

デフォルトのセッションストレージ

まずは、デフォルトのセッションストレージ CookieStore を確認してみます。
2.3 セッションストレージ | Rails セキュリティガイド - Railsガイド

 

Railsアプリの作成

Railsアプリをゼロから作成してきます。今回は rails_dalli_sample とします。

% rails new rails_dalli_sample

 
今回の動作確認ではwebpacker使いません。そのため、Gemfileでコメントアウトしておきます。

source 'https://rubygems.org'
...
# Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker
#gem 'webpacker', '~> 5.0'  # コメントアウト

 
また、Railsでの memcached クライアントは Dalli gem のため、Gemfileに追加します。
petergoldstein/dalli: High performance memcached client for Ruby

gem 'dalli'

 
改めて bundle install します。

% bundle install

 
続いて、ControllerとViewを生成します。

今回は home Controller に index メソッドをもたせます。

% bin/rails g controller home index --helper=false --assets=false
Running via Spring preloader in process 21510
      create  app/controllers/home_controller.rb
       route  get 'home/index'
      invoke  erb
       exist    app/views/home
      create    app/views/home/index.html.erb
      invoke  test_unit
      create    test/controllers/home_controller_test.rb

 
作成したControllerの index メソッドで、セッションに値を入れます。
5.1 セッションにアクセスする | Action Controller の概要 - Railsガイド

今回は key を foo 、値を bar とします。

class HomeController < ApplicationController
  def index
    session[:foo] = 'bar'
  end
end

 
Viewは自動生成のものを流用します。

ただ、今回はwebpackerを使わないため、layoutファイル (app/views/layouts/application.html.erb) から javascript_pack_tag タグの部分をコメントアウトしておきます。

<%#= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>

 
また、これは任意ですが、手元のRailsが複数ある場合はRailsの起動ポートを変えておきます。
rails s 時のデフォルトのポート番号を変更する - Qiita

# config/puma.rb
port ENV.fetch("PORT") { 3700 }

 

動作確認

準備ができたので、Railsを起動します。

% bin/rails s
=> Booting Puma
=> Rails 6.1.3.2 application starting in development 
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Puma version: 5.3.2 (ruby 3.0.1-p64) ("Sweetnighter")
*  Min threads: 5
*  Max threads: 5
*  Environment: development
*          PID: 21698
* Listening on http://127.0.0.1:3700
* Listening on http://[::1]:3700
Use Ctrl-C to stop

 
ブラウザで http://localhost:3700/home/index へアクセスすると、Cookieに暗号化された値が保存されています。

 

キャッシュの保存先を変更するための準備

セッションの保存先を変える前に、まずはRailsのキャッシュの保存先をmemcachedへと移動してみます。

 

フラグメントキャッシュを使うために erb を修正

今回はフラグメントキャッシュを有効化してみます。

フラグメントキャッシュとして保存するよう、 View ( app/views/home/index.html.erb ) に追記します。

<% cache 'my_cache' do %>
<h1>Home#index</h1>
<p>Find me in app/views/home/index.html.erb</p>
<% end %>

 

環境ごとのDB設定を追加

今回は色々なパターンを確認することから、いくつもの環境設定を用意します。

DBは使わないものの、環境に応じた設定がされていないとエラーになることから、以下の設定を config/database.yml の末尾へ追加します。

cache:
  <<: *default
  database: db/development.sqlite3

cache_port:
  <<: *default
  database: db/development.sqlite3

memd_session:
  <<: *default
  database: db/development.sqlite3

memd_session_port:
  <<: *default
  database: db/development.sqlite3

session_cache_store:
  <<: *default
  database: db/development.sqlite3

 

docker composeによる memcached を用意

次に、memcachedを用意します。

今回は docker compose を使って memcached を3つたてます。

service名 mac上のポート 用途
default 11211 デフォルトポートで起動するmemcached
cache 17001 Cache用memcached
session 17002 Session用memcached

 
docker-compose.ymlはこんな感じです。

version: "3"
services:
  default:
    image: memcached
    ports:
      - 11211:11211
  cache:
    image: memcached
    ports:
      - 17001:11211
  session:
    image: memcached
    ports:
      - 17002:11211

 
docker compose で memcached を起動しておきます。
Compose CLI Tech Preview | Docker Documentation

% docker compose up -d

 

memcached の中身を確認するPythonスクリプトを作成

memcachedの中身の確認ですが、RubyMineではさくっとできなかったため、別の方法を探してみました。

Pythonのライブラリを探したところ

を組み合わせれば良さそうでした。

そこで、コマンドライン引数としてポートを渡せば中身を確認できるようなスクリプト

import sys
from pymemcache.client.base import Client
from memcached_stats import MemcachedStats

if __name__ == '__main__':
    port = sys.argv[1]
    mem = MemcachedStats('localhost', port)

    client = Client(f'localhost:{port}')
    for key in mem.keys():
        print(client.gets(key))

を作りました。

これで準備は完了です。

 

デフォルトポートの memcached へキャッシュを保存

環境ファイルの作成

まずは、docker compose 上にある、デフォルトポートの memcached へキャッシュを保存してみます。

今回の記事では色々なパターンを試すことから、 config/environments ディレクトリの中に、各パターンの環境を作成します。
3.20 Rails環境を作成する | Rails アプリケーションを設定する - Railsガイド

今回は development.rb をコピーし、各パターンの環境を作成します。

ここでは、 cache.rb を作成し、デフォルトポートの memcached へキャッシュを保存する設定を行います。

まずは既存の設定

# Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching.
if Rails.root.join('tmp', 'caching-dev.txt').exist?
  config.action_controller.perform_caching = true
  config.action_controller.enable_fragment_cache_logging = true

  config.cache_store = :memory_store
  config.public_file_server.headers = {
    'Cache-Control' => "public, max-age=#{2.days.to_i}"
  }
else
  config.action_controller.perform_caching = false

  config.cache_store = :null_store
end

を削除します。

次に、デフォルトポートの memcached を使うよう設定します。
2.5 ActiveSupport::Cache::MemCacheStore | Rails のキャッシュ機構 - Railsガイド

config.cache_store = :mem_cache_store

 

動作確認

準備ができたので、 環境 cache を指定して起動します。
1.2 rails server | Rails のコマンドラインツール - Railsガイド

% bin/rails s -e cache

 
ブラウザで http://localhost:3700/home/index へアクセスした後、 memcached の状態をPythonスクリプトで確認します。

ポート 11211 のmemcached のみ、データが格納されていました。

% python display_memcached.py 11211
(b'\x04\x08o: ActiveSupport::Cache::Entry\t:\x0b@valueI"I<h1>Home#index</h1>\n<p>Find me in app/views/home/index.html.erb</p>\n\x06:\x06ET:\r@versionI"\x00\x06;\x07F:\x10@created_atf\x171622552901.4518201:\x10@expires_in0', b'1')
...

 
次の確認を行う前に、すべての memcached を再起動し、Pythonスクリプトでデータがないことを確認しておきます。

なお、以降の確認後も同じ作業を実施するものとします。

 

キャッシュをキャッシュ用memcachedへと移動

続いて、キャッシュをキャッシュ用memcachedへと移動します。

環境ファイルの作成

ホストとポートを指定するには、 config.cache_store の設定を追加すれば良さそうです。

キャッシュの初期化時には、クラスタ内の全memcachedサーバーのアドレスを指定する必要があります。指定がない場合、memcachedがローカルのデフォルトポートで動作していると仮定して起動しますが、この設定は大規模サイトには向いていません。

Rails のキャッシュ機構 - Railsガイド

 
そのため、上記で作成した cache.rb をコピーした cache_port.rb ファイルをenvironments の中に用意し、以下の設定に書き換えます。

config.cache_store = :mem_cache_store, 'localhost:17001'

 

動作確認

環境を指定して起動します。

% bin/rails s -e cache_port

 
ブラウザで http://localhost:3700/home/index へアクセスした後、 memcached の状態をPythonスクリプトで確認すると、ポート 17001 のmemcachedにキャッシュが保存されていました。

# 空
% python display_memcached.py 11211

# 指定したポートの memcached にキャッシュが保存
% python display_memcached.py 17001
(b'\x04\x08o: ActiveSupport::Cache::Entry\t:\x0b@valueI"I<h1>Home#index</h1>\n<p>Find me in app/views/home/index.html.erb</p>\n\x06:\x06ET:\r@versionI"\x00\x06;\x07F:\x10@created_atf\x161622554358.104258:\x10@expires_in0', b'3')
(b'\x04\x08o: ActiveSupport::Cache::Entry\t:\x0b@valueI"I<h1>Home#index</h1>\n<p>Find me in app/views/home/index.html.erb</p>\n\x06:\x06ET:\r@versionI"\x00\x06;\x07F:\x10@created_atf\x161622554358.104258:\x10@expires_in0', b'3')
(b'\x04\x08o: ActiveSupport::Cache::Entry\t:\x0b@valueI"I<h1>Home#index</h1>\n<p>Find me in app/views/home/index.html.erb</p>\n\x06:\x06ET:\r@versionI"\x00\x06;\x07F:\x10@created_atf\x161622554358.104258:\x10@expires_in0', b'3')
(b'\x04\x08o: ActiveSupport::Cache::Entry\t:\x0b@valueI"I<h1>Home#index</h1>\n<p>Find me in app/views/home/index.html.erb</p>\n\x06:\x06ET:\r@versionI"\x00\x06;\x07F:\x10@created_atf\x161622554358.104258:\x10@expires_in0', b'3')

# こちらも空
% python display_memcached.py 17002

 

セッションストレージをデフォルトのmemcachedへ移動

Rails6.1では dalli_store を指定しても動作しない

キャッシュの移動ができたため、次はセッションストレージを移動します。

設定方法については、DalliのWikiに記載がありました。
Caching with Rails · petergoldstein/dalli Wiki

 
そこで、memd_session.rb という環境ファイルを用意し

config.session_store = :dalli_store, 'localhost:11211'

と設定します。

続いて、

% bin/rails s -e memd_session

とした後、ブラウザでアクセスしてみましたが、セッションストレージはCookie Storeのままでした。

 
調べてみたところ、Rails 5.2でワーニングが出るようになり、:dalli_store から :mem_cache_store へと変更されたようです。

 

Rails6.1では mem_cache_store を指定する

memd_session.rb を修正します。

# 指定したポートにある memcached へCacheを保存
config.cache_store = :mem_cache_store, 'localhost:17001'

# セッションストアを memcached へ変更
config.session_store :mem_cache_store   # << 変更箇所

 

動作確認

再度 bin/rails s -e memd_session で起動し、ブラウザで http://localhost:3700/home/index へアクセスします。

Cookieのキー _rails_dalli_sample_session の代わりに _session_id がありました。また、値もセッションIDだけになっています。

 
続いて、memcachedの値を確認します。

フラグメントキャッシュは、指定通り 17001 ポートにありました。

% python display_memcached.py 17001
(b'\x04\x08o: ActiveSupport::Cache::Entry\t:\x0b@valueI"I<h1>Home#index</h1>\n<p>Find me in app/views/home/index.html.erb</p>\n\x06:\x06ET:\r@versionI"\x00\x06;\x07F:\x10@created_atf\x161622554618.821114:\x10@expires_in0', b'5')
...

 
一方、セッションの値はデフォルトポートのmemcachedに入っていました。

\x07I"\x08foo\x06:\x06EFI"\x08bar\x06; のようにして、 foo=bar なセッションの値が確認できました。

% python display_memcached.py 11211
(b'\x04\x08{\x07I"\x08foo\x06:\x06EFI"\x08bar\x06;\x00TI"\x10_csrf_token\x06;\x00FI"1D7iFliPuRIrsP1D1GfYM0lr8WhM3xKdaPWDRod9Uhs0=\x06;\x00F', b'6')
...

 

セッションストレージをsession用のmemcachedへ移動

コードを読んで、設定方法を調査

ポートを指定する方法がRailsガイドでは分かりませんでした。

そこで、Dalliの公式Wikiにあったように、

config.session_store :mem_cache_store, 'localhost:17002'

と、第2引数にホストとポートを指定して起動してみたところ

`session_store': wrong number of arguments (given 2, expected 0..1) (ArgumentError)

というエラーになりました。

 
そこでRailsガイドのセッションストレージの記載

ActionDispatch::Session::MemCacheStore :データをmemcachedクラスタに保存する (この実装は古いのでCacheStoreを検討すべき)

5 セッション | Action Controller の概要 - Railsガイド

より、 ActionDispatch::Session::MemCacheStore の実装を見ると

# https://github.com/rails/rails/blob/v6.1.3.2/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb#L17

class MemCacheStore < Rack::Session::Dalli
# ...

となっていました。

次に Rack::Session::Dalli の実装を見てみると

# https://github.com/petergoldstein/dalli/blob/v2.7.11/lib/rack/session/dalli.rb#L10

module Rack
  module Session
    class Dalli < defined?(Abstract::Persisted) ? Abstract::Persisted : Abstract::ID
      attr_reader :pool, :mutex

      DEFAULT_DALLI_OPTIONS = {
        :namespace => 'rack:session',
        :memcache_server => 'localhost:11211'
      }
# ...

と、オプション :memcache_server としてセッション用のmemcachedを渡せそうでした。

 

設定

調査結果をもとに、環境ファイル memd_session_port.rb

# 指定したポートにある memcached へCacheを保存
config.cache_store = :mem_cache_store, 'localhost:17001'

# セッションストアを memcached へ変更し、ポートも指定する
config.session_store :mem_cache_store, memcache_server: 'localhost:17002'

と設定しました。

 

動作確認

上記で作成した環境ファイルを指定して

% bin/rails s -e memd_session_port

と起動すると

  • キャッシュはキャッシュ用memcached (17001ポート)
  • セッションはセッション用memcached (17002ポート)

に保存されていました。

# 無い
% python display_memcached.py 11211

# Cache用
% python display_memcached.py 17001
(b'\x04\x08o: ActiveSupport::Cache::Entry\t:\x0b@valueI"I<h1>Home#index</h1>\n<p>Find me in app/views/home/index.html.erb</p>\n\x06:\x06ET:\r@versionI"\x00\x06;\x07F:\x10@created_atf\x151622644680.48053:\x10@expires_in0', b'1')
...

# セッション用
% python display_memcached.py 17002
(b'\x04\x08{\x07I"\x08foo\x06:\x06EFI"\x08bar\x06;\x00TI"\x10_csrf_token\x06;\x00FI"1IseQM7XUAmDJwXYDllu0OwpS8DpoLCTRjATi8kIzOGE=\x06;\x00F', b'2')
...

 
想定通りの設定ができたようです。

 

キャッシュとセッションを同じ memcached へ保存する

当初の目的 セッションをキャッシュとは別のmemcachedへ保存する は達成したものの、先ほど見たRailsガイド

ActionDispatch::Session::MemCacheStore :データをmemcachedクラスタに保存する (この実装は古いのでCacheStoreを検討すべき)

5 セッション | Action Controller の概要 - Railsガイド

この実装は古いのでCacheStoreを検討すべき が気になりました。

また、「Action Controller の概要」にも

ユーザーセッションに重要なデータが含まれていない場合、またはユーザーセッションを長期間保存する必要がない場合 (flashメッセージで使いたいだけの場合など) は、ActionDispatch::Session::CacheStoreを検討してください。この方式では、Webアプリケーションに設定されているキャッシュ実装を利用してセッションを保存します。この方法のよい点は、既存のキャッシュインフラをそのまま利用してセッションを保存できることと、管理用の設定を追加する必要がないことです。この方法の欠点はセッションが短命になり、セッションがいつでも消える可能性がある点です。

5 セッション | Action Controller の概要 - Railsガイド

とありました。

そこで CacheStore も試してみます。

 

設定

Railsガイドによると、

config.session_store: セッションの保存に使うクラスを指定します。指定できる値は:cookie_store(デフォルト)、:mem_cache_store、:disabledです。:disabledを指定すると、Railsでセッションが扱われなくなります。デフォルトでは、アプリケーション名と同じ名前のcookieストアがセッションキーとして使われます。カスタムセッションストアを指定することもできます。

config.session_store :my_custom_store

カスタムストアはActionDispatch::Session::MyCustomStoreとして定義する必要があります。

3.1 Rails全般の設定 | Rails アプリケーションを設定する - Railsガイド

とありました。

 
ActionDispatch::Session::CacheStore はすでに存在していることから、Railsガイドに従って環境ファイル session_cache_store.rb

# 指定したポートにある memcached へCacheを保存
config.cache_store = :mem_cache_store, 'localhost:17001'

# セッションストアを "ActionDispatch::Session::CacheStore" にする
config.session_store :cache_store

# ちなみに、以下の書き方でも動作した
# config.session_store ActionDispatch::Session::CacheStore

に、キャメルケースをスネークケースに変換したシンボル :cache_store で指定します。

 

動作確認

上記で作成した環境ファイルを指定して

% bin/rails s -e session_cache_store

と起動してアクセス後に確認したところ、

# 無い
% python display_memcached.py 11211

# キャッシュとセッションが同居
% python display_memcached.py 17001
(b'\x04\x08o: ActiveSupport::Cache::Entry\t:\x0b@value{\x07I"\x08foo\x06:\x06EFI"\x08bar\x06;\x07TI"\x10_csrf_token\x06;\x07FI"1-z6lVxxcPeSrD6sBI0LsfxUWlmsIVr7ybExtJix2uX8=\x06;\x07F:\r@version0:\x10@created_atf\x161622879435.064419:\x10@expires_in0', b'2')
...
(b'\x04\x08o: ActiveSupport::Cache::Entry\t:\x0b@valueI"I<h1>Home#index</h1>\n<p>Find me in app/views/home/index.html.erb</p>\n\x06:\x06ET:\r@versionI"\x00\x06;\x07F:\x10@created_atf\x161622879435.008329:\x10@expires_in0', b'1')
...

# 無い
% python display_memcached.py 17002

と、キャッシュとセッションが同居していました。

 
ちなみに、

config.cache_store = :file_store, Rails.root.join('tmp', 'cache', 'files')
config.session_store :cache_store

とすると、キャッシュとはファイルが別だったものの、セッションもファイルストレージに保存されていました。

こんな感じです。

^D^Ho: ActiveSupport::Cache::Entry      :^K@value{^GI"^Hfoo^F:^FEFI"^Hbar^F;^GTI"^P_csrf_token^F;^GFI"1Va3rVZwaQqxekgbKJHpN6dyA5JEyFtrbrO9kWqmOdfs=^F;^GF:^M@version0:^P@created_atf^W1622870283.7916899:^P@expires_in0

 

ソースコード

Githubに上げました。
thinkAmi-sandbox/rails_session_of_memcached-sample

PyCharmで、anyenv + nodenv で構築した ReactとDjango REST Frameworkの両方をデバッグしてみた

今までPyCharmでDjango REST framework(以下、DRF)のデバッグを行ったことはありました。

そんな中、以前 React + TypeScript + DRFでアプリを作りました。

ただ、ReactとDRFをPyCharmだけでデバッグしたことがなかったため、どのように設定すればデバッグできるか試してみました。

 
目次

 

環境

 

DRFを起動する設定

PyCharmでDRFを開発していれば、以下の設定があるはず。。。

項目
Configuration Template Django Server
Name 任意 (backendなど)
Host localhost
Port 8000
Environment variables (デフォルト値)
Python interpreter Projectで使ってる venv のインタプリタ
Add content roots to PYTHONPATH チェックする
Add source roots to PYTHONPATH チェックする

 

yarnでReactを起動する設定

React側は

の2つが必要になります。

まずは、yarnでReactを起動する設定です。

項目
Configuration Template npm
Name 任意
package.json [Reactアプリのpackage.json forntend/package.json のパス
Command start
Node interpreter anyenv + nodenvで入れたNodeのパス (~/.anyenv/envs/nodenv/versions/<version>/bin/node )
Package manager yarn (~/.anyenv/envs/nodenv/versions/<version>/lib/node_modules/yarn)

 
yarnの設定後、実行ボタンをクリックし、Reactが起動すればOKです。

Runのログにはこんな感じで表示されます。

yarn run v1.22.10
$ react-scripts start
...
Starting the development server...

 

JavaScriptデバッグ設定

yarnでReactを起動する設定を行いデバッグ実行したとしても、まだブレークポイントでは停止しません。

そこで、JavaScriptデバッグ設定を追加します。

項目
Configuration Template JavaScript Debug
Name 任意
URL [Reactアプリを起動した時のURL ( http://localhost:3000 など)
Browser Chrome

 
以上で準備ができました。

 

動作確認

流れとしては、

  1. DRFデバッグ起動
  2. Reactを Run で起動
  3. JavaScript Debugを Debug で起動

の順で起動します。

また、DRFとReactの両方にブレークポイントを置いてみます。

 
3. の実行後、拡張機能が何もインストールされていない別のChromeウィンドウが起動します*1

試しに適当な値を入力し、「登録」ボタンを押してみます。 

 
まずはReactのところで止まりました。

f:id:thinkAmi:20210524211014p:plain

 
続行してみると、DRFの方でも止まりました。

f:id:thinkAmi:20210524211153p:plain

 

なお、Reactのコードを修正したところ、Chromeにも反映されました。

f:id:thinkAmi:20210524211405p:plain

 
これで良さそうです。

*1:今回はReactをポート3600で起動していますが、3000でも問題ないはず

rake -T では表示されないRakeタスクについて

Rakefileの中に定義されているRakeタスクを確認しようと、 rake -T したところ表示されないRakeタスクがあったため、メモを残します。

 

環境

  • rake 13.0.2

 

原因

ヘルプに書いてありました。

rake -T では desc がないRakeタスクは表示されないとのことでした。

% rake --help

-T, --tasks [PATTERN]            Display the tasks (matching optional PATTERN) with descriptions, then exit. -AT combination displays all of tasks contained no description.

 

確認

こんな感じでRakefileを作ってみました。

desc "シナノゴールド"
task :shinanogold do
    puts '黄色'
end

desc ""
task :fuji do
    puts ''
end

desc nil
task :orin do
    puts ''
end

task :pinklady do
    puts 'ピンク'
end

 
rake -T の場合、descがあるものだけ表示されました。

% rake -T
rake shinanogold  # シナノゴールド

 
一方、 rake -TA の場合、すべて表示されました。

% rake -AT
rake fuji         # 
rake orin         # 
rake pinklady     # 
rake shinanogold  # シナノゴールド

 
なお、 rake -TA では表示されませんでした。

% rake -TA #=> 何も表示されない

 
他に、 rake -P などでも表示されるようです。
ruby on rails - Why is rake db:migrate:reset not listed in rake -T? - Stack Overflow

今回はgrepを渡さなくても、全件表示されました。

% rake -P
rake fuji
rake orin
rake pinklady
rake shinanogold

 
また、D オプションと組み合わせた時はこんな感じでした。

% rake -D 
rake shinanogold
    シナノゴールド


% rake -AD
rake fuji

rake orin

rake pinklady

rake shinanogold
    シナノゴールド

Delayed Job を使って実行する処理を、RubyMineでデバッグをする

Rails + Delayed Job な環境で、Delayed Job を使って実行する処理がありました。

例えばこんな感じです。

# Delayed Job で実行
# call_heavy_api中で外部APIを呼んでいるが、その処理が重いとする
Task.delay.call_heavy_api({ foo: 'bar' })

 
そんな中、Delayed Job を使って実行する処理 (上記例では call_heavy_api の中身) をデバッグしたくなったところ、同僚からやり方を聞いたため、メモを残します。

 
目次

 

環境

  • macOS
  • RubyMine 2021.1.1

 

設定

Delayed Jobs は Rake タスクなので、以下の設定を RubyMine に行います。

  • Run > Edit Configurations ...
  • Rakeタスクを追加
    • Name: 任意
    • Configurationタブ
      • Task name: jobs:work
    • Bundlerタブ
      • Run the script in context of the bundle (bundle exec) にチェックを入れる
        • bundlerを使っているため

 

実際のアプリで確認

Rails + Delayed Job なアプリを作って確認してみます。

 

Railsアプリを作成
環境構築
# railsの準備
% bundle init

 
Gemfileを作成

# Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "rails"

 
APIRailsアプリを作成

% rails new delayed_job_debug --api

% cd delayed_job_debug

 
delayed_job_debugのGemfileに、Delayed JobをActiveRecordで使うためのgemを追記。

# delayed_job_debug/Gemfile

# ...
gem 'delayed_job_active_record'

 
インストール

bundle install

 
Delayed Job の準備をします。

% bin/rails g delayed_job:active_record
Running via Spring preloader in process 13171
      create  bin/delayed_job
       chmod  bin/delayed_job
      create  db/migrate/20210517154917_create_delayed_jobs.rb

% bin/rake db:migrate
Running via Spring preloader in process 13208
== 20210517154917 CreateDelayedJobs: migrating ================================
-- create_table(:delayed_jobs)
   -> 0.0035s
-- add_index(:delayed_jobs, [:priority, :run_at], {:name=>"delayed_jobs_priority"})
   -> 0.0013s
== 20210517154917 CreateDelayedJobs: migrated (0.0050s) =======================

 
config/application.rb にて、Active JobのバックエンドとしてDelayed Job を指定

# config/application.rb
config.active_job.queue_adapter = :delayed_job

 

アプリ追加

重いAPIを呼んでいるモデルを作ります。

今回はDBがなくても確認できるため、モデルファイル app/models/task.rb を作るだけにします。

class Task
  class << self
    # 外部APIの処理が重いとする
    def call_heavy_api(params)
      Rails.logger.warn(params)

      'success'
    end
  end
end

 
上記のメソッドを呼んでいるコントローラを作ります。

% bin/rails g controller tasks
Running via Spring preloader in process 13248
      create  app/controllers/tasks_controller.rb
      invoke  test_unit

 
生成されたファイル app/controllers/task_controller.rb に追記します。

class TasksController < ApplicationController
  def create
    Task.delay.call_heavy_api({ foo: 'bar' })

    render json: { status: 'SUCCESS', data: 'done' }
  end
end

 
config/routes へ追記します。

Rails.application.routes.draw do
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
  post 'tasks/' => 'tasks#create'
end

 

Railsアプリの動作確認

実行します。
f:id:thinkAmi:20210519072647p:plain

 

TerminalからcurlでPOSTします。

% curl -X POST -H "Content-Type: application/json" -d '{}' http://127.0.0.1:3500/tasks/

{"status":"SUCCESS","data":"done"}

 
DBに値が入っています。

f:id:thinkAmi:20210519072635p:plain

 
Delayed Job をターミナルから実行します。

% bin/rake jobs:work

Running via Spring preloader in process 32682
[Worker(host:kamijonoMacBook-Pro.local pid:32682)] Starting job worker
[Worker(host:kamijonoMacBook-Pro.local pid:32682)] Job Task.call_heavy_api (id=3) RUNNING
[Worker(host:kamijonoMacBook-Pro.local pid:32682)] Job Task.call_heavy_api (id=3) COMPLETED after 0.0080
[Worker(host:kamijonoMacBook-Pro.local pid:32682)] 1 jobs processed at 12.7021 j/s, 0 failed

 
Sever development log に実行内容も表示されました。

[Worker(host:kamijonoMacBook-Pro.local pid:32682)] Job Task.call_heavy_api (id=3) RUNNING
{:foo=>"bar"}

 
確認できたため、TerminalでDelayed Job をキャンセルしておきます。

 

デバッグ実行
RubyMineの設定

メニューの Run > Edit Configurations... を選択します。

左上の + ボタンを押して Rake を選択し、以下の内容を追加します。

項目
Name 任意 (job)
Configuration - Task name jobs:work
Bundler - Run the script... チェックを入れる

 

動作確認

Terminalから curl で POST し、DBに保存されることを確認します。

% curl -X POST -H "Content-Type: application/json" -d '{}' http://127.0.0.1:3500/tasks/
{"status":"SUCCESS","data":"done"}

 
確認したいところにブレークポイントを設置します。

f:id:thinkAmi:20210519072617p:plain

 
先ほど作成した設定を選択し、デバッグ実行します。

なお、初回は ruby-debug-ide のインストールが提案されるため、インストールします。

f:id:thinkAmi:20210519072600p:plain

 
しばらく待つと、ブレークポイントで止まりました。変数の中身なども確認できます。

f:id:thinkAmi:20210519072544p:plain

 

ソースコード

確認したRailsアプリはGithubに上げました。
https://github.com/thinkAmi-sandbox/delayed_job_debug-sample